├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── gradle.xml
├── misc.xml
├── modules.xml
└── runConfigurations.xml
├── app
├── .gitignore
├── app-free-release.apk
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── delaroystudios
│ │ └── inappsubscription
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── delaroystudios
│ │ │ └── inappsubscription
│ │ │ ├── Constants.java
│ │ │ ├── DashboardActivity.java
│ │ │ ├── MainActivity.java
│ │ │ └── util
│ │ │ ├── IabBroadcastReceiver.java
│ │ │ ├── IabException.java
│ │ │ ├── IabHelper.java
│ │ │ ├── IabResult.java
│ │ │ ├── Inventory.java
│ │ │ ├── Purchase.java
│ │ │ ├── Security.java
│ │ │ └── SkuDetails.java
│ └── res
│ │ ├── drawable
│ │ └── subscribe.png
│ │ ├── layout
│ │ ├── dashboard_activity.xml
│ │ └── subscribe_layout.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── delaroystudios
│ └── inappsubscription
│ └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/app-free-release.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/app-free-release.apk
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion 26
5 | buildToolsVersion "26.0.1"
6 | signingConfigs {
7 | config {
8 | keyAlias 'rasbitakey'
9 | keyPassword 'RasbitA'
10 | storeFile file('/home/delaroy/Music/keystore/rasbitakey.jks')
11 | storePassword 'RasbitA'
12 | }
13 | }
14 | defaultConfig {
15 | applicationId "com.delaroystudios.inappsubscription"
16 | minSdkVersion 15
17 | targetSdkVersion 26
18 | versionCode 1
19 | versionName "1.0"
20 | signingConfig signingConfigs.config
21 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
22 | }
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
27 | signingConfig signingConfigs.config
28 | }
29 | }
30 |
31 | productFlavors {
32 | free {
33 | minSdkVersion 15
34 | applicationId 'com.delaroystudios.inappsubscription'
35 | signingConfig signingConfigs.config
36 | targetSdkVersion 26
37 | testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
38 | versionCode 1
39 | versionName '1.1'
40 | }
41 | }
42 | }
43 |
44 | dependencies {
45 | compile fileTree(dir: 'libs', include: ['*.jar'])
46 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
47 | exclude group: 'com.android.support', module: 'support-annotations'
48 | })
49 | compile 'com.android.support:appcompat-v7:26.+'
50 | compile 'com.android.support.constraint:constraint-layout:1.0.2'
51 | compile 'com.android.billingclient:billing:1.0'
52 | testCompile 'junit:junit:4.12'
53 | }
54 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /home/delaroy/opt/android-sdk-linux/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 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/delaroystudios/inappsubscription/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.delaroystudios.inappsubscription;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.delaroystudios.inappsubscription", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/Constants.java:
--------------------------------------------------------------------------------
1 | package com.delaroystudios.inappsubscription;
2 |
3 | /**
4 | * Created by delaroy on 11/20/17.
5 | */
6 |
7 | public class Constants {
8 |
9 | public static final String SKU_DELAROY_MONTHLY = "delaroy_monthly";
10 | public static final String SKU_DELAROY_THREEMONTH = "delaroy_threemonth";
11 | public static final String SKU_DELAROY_SIXMONTH = "delaroy_sixmonth";
12 | public static final String SKU_DELAROY_YEARLY = "delaroy_yearly";
13 |
14 | public static final String base64EncodedPublicKey = "";
15 |
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/DashboardActivity.java:
--------------------------------------------------------------------------------
1 | package com.delaroystudios.inappsubscription;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 |
6 | /**
7 | * Created by delaroy on 11/20/17.
8 | */
9 |
10 | public class DashboardActivity extends AppCompatActivity {
11 |
12 | @Override
13 | public void onCreate(Bundle savedInstanceState) {
14 | super.onCreate(savedInstanceState);
15 |
16 | setContentView(R.layout.dashboard_activity);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.delaroystudios.inappsubscription;
2 |
3 | import android.app.Activity;
4 | import android.app.AlertDialog;
5 | import android.content.DialogInterface;
6 | import android.content.Intent;
7 | import android.content.IntentFilter;
8 | import android.support.v7.app.AppCompatActivity;
9 | import android.os.Bundle;
10 | import android.text.TextUtils;
11 | import android.util.Log;
12 | import android.view.View;
13 | import android.widget.ImageView;
14 |
15 | import com.delaroystudios.inappsubscription.util.IabBroadcastReceiver;
16 | import com.delaroystudios.inappsubscription.util.IabHelper;
17 | import com.delaroystudios.inappsubscription.util.IabResult;
18 | import com.delaroystudios.inappsubscription.util.Inventory;
19 | import com.delaroystudios.inappsubscription.util.Purchase;
20 |
21 | import java.util.ArrayList;
22 | import java.util.List;
23 |
24 | import static com.delaroystudios.inappsubscription.Constants.SKU_DELAROY_MONTHLY;
25 | import static com.delaroystudios.inappsubscription.Constants.SKU_DELAROY_SIXMONTH;
26 | import static com.delaroystudios.inappsubscription.Constants.SKU_DELAROY_THREEMONTH;
27 | import static com.delaroystudios.inappsubscription.Constants.SKU_DELAROY_YEARLY;
28 | import static com.delaroystudios.inappsubscription.Constants.base64EncodedPublicKey;
29 |
30 | public class MainActivity extends AppCompatActivity implements IabBroadcastReceiver.IabBroadcastListener,
31 | DialogInterface.OnClickListener {
32 | // Debug tag, for logging
33 | static final String TAG = "MainActivity";
34 |
35 | // Does the user have an active subscription to the delaroy plan?
36 | boolean mSubscribedToDelaroy = false;
37 |
38 | // Will the subscription auto-renew?
39 | boolean mAutoRenewEnabled = false;
40 |
41 | // Tracks the currently owned subscription, and the options in the Manage dialog
42 | String mDelaroySku = "";
43 | String mFirstChoiceSku = "";
44 | String mSecondChoiceSku = "";
45 | String mThirdChoiceSku = "";
46 | String mFourthChoiceSku = "";
47 |
48 | // Used to select between subscribing on a monthly, three month, six month or yearly basis
49 | String mSelectedSubscriptionPeriod = "";
50 |
51 | // SKU for our subscription
52 |
53 | // (arbitrary) request code for the purchase flow
54 | static final int RC_REQUEST = 10001;
55 |
56 |
57 | // The helper object
58 | IabHelper mHelper;
59 |
60 | // Provides purchase notification while this app is running
61 | IabBroadcastReceiver mBroadcastReceiver;
62 | @Override
63 | public void onCreate(Bundle savedInstanceState) {
64 | super.onCreate(savedInstanceState);
65 |
66 | setContentView(R.layout.subscribe_layout);
67 |
68 | // Create the helper, passing it our context and the public key to verify signatures with
69 | Log.d(TAG, "Creating IAB helper.");
70 | mHelper = new IabHelper(this, base64EncodedPublicKey);
71 |
72 | // enable debug logging (for a production application, you should set this to false).
73 | mHelper.enableDebugLogging(true);
74 |
75 | // Start setup. This is asynchronous and the specified listener
76 | // will be called once setup completes.
77 | Log.d(TAG, "Starting setup.");
78 | mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
79 | public void onIabSetupFinished(IabResult result) {
80 | Log.d(TAG, "Setup finished.");
81 |
82 | if (!result.isSuccess()) {
83 | // Oh noes, there was a problem.
84 | complain("Problem setting up in-app billing: " + result);
85 | return;
86 | }
87 |
88 | // Have we been disposed of in the meantime? If so, quit.
89 | if (mHelper == null) return;
90 |
91 | mBroadcastReceiver = new IabBroadcastReceiver(MainActivity.this);
92 | IntentFilter broadcastFilter = new IntentFilter(IabBroadcastReceiver.ACTION);
93 | registerReceiver(mBroadcastReceiver, broadcastFilter);
94 |
95 | // IAB is fully set up. Now, let's get an inventory of stuff we own.
96 | Log.d(TAG, "Setup successful. Querying inventory.");
97 | try {
98 | mHelper.queryInventoryAsync(mGotInventoryListener);
99 | } catch (IabHelper.IabAsyncInProgressException e) {
100 | complain("Error querying inventory. Another async operation in progress.");
101 | }
102 | }
103 | });
104 | }
105 |
106 | // Listener that's called when we finish querying the items and subscriptions we own
107 | IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
108 | public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
109 | Log.d(TAG, "Query inventory finished.");
110 |
111 | // Have we been disposed of in the meantime? If so, quit.
112 | if (mHelper == null) return;
113 |
114 | // Is it a failure?
115 | if (result.isFailure()) {
116 | complain("Failed to query inventory: " + result);
117 | return;
118 | }
119 |
120 | Log.d(TAG, "Query inventory was successful.");
121 |
122 |
123 | // First find out which subscription is auto renewing
124 | Purchase delaroyMonthly = inventory.getPurchase(SKU_DELAROY_MONTHLY);
125 | Purchase delaroyThreeMonth = inventory.getPurchase(SKU_DELAROY_THREEMONTH);
126 | Purchase delaroySixMonth = inventory.getPurchase(SKU_DELAROY_SIXMONTH);
127 | Purchase delaroyYearly = inventory.getPurchase(SKU_DELAROY_YEARLY);
128 | if (delaroyMonthly != null && delaroyMonthly.isAutoRenewing()) {
129 | mDelaroySku = SKU_DELAROY_MONTHLY;
130 | mAutoRenewEnabled = true;
131 | } else if (delaroyThreeMonth != null && delaroyThreeMonth.isAutoRenewing()) {
132 | mDelaroySku = SKU_DELAROY_THREEMONTH;
133 | mAutoRenewEnabled = true;
134 | } else if (delaroySixMonth != null && delaroySixMonth.isAutoRenewing()){
135 | mDelaroySku = SKU_DELAROY_SIXMONTH;
136 | mAutoRenewEnabled = true;
137 | } else if (delaroyYearly != null && delaroyYearly.isAutoRenewing()){
138 | mDelaroySku = SKU_DELAROY_YEARLY;
139 | mAutoRenewEnabled = true;
140 | }
141 |
142 | else {
143 | mDelaroySku = "";
144 | mAutoRenewEnabled = false;
145 | }
146 |
147 | // The user is subscribed if either subscription exists, even if neither is auto
148 | // renewing
149 | mSubscribedToDelaroy = (delaroyMonthly != null && verifyDeveloperPayload(delaroyMonthly))
150 | || (delaroyThreeMonth != null && verifyDeveloperPayload(delaroyThreeMonth))
151 | || (delaroySixMonth != null && verifyDeveloperPayload(delaroySixMonth))
152 | || (delaroyYearly != null && verifyDeveloperPayload(delaroyYearly));
153 | Log.d(TAG, "User " + (mSubscribedToDelaroy ? "HAS" : "DOES NOT HAVE")
154 | + " infinite gas subscription.");
155 |
156 | updateUi();
157 | setWaitScreen(false);
158 | Log.d(TAG, "Initial inventory query finished; enabling main UI.");
159 | }
160 | };
161 |
162 | @Override
163 | public void receivedBroadcast() {
164 | // Received a broadcast notification that the inventory of items has changed
165 | Log.d(TAG, "Received broadcast notification. Querying inventory.");
166 | try {
167 | mHelper.queryInventoryAsync(mGotInventoryListener);
168 | } catch (IabHelper.IabAsyncInProgressException e) {
169 | complain("Error querying inventory. Another async operation in progress.");
170 | }
171 | }
172 |
173 | // "Subscribe to delaroy" button clicked. Explain to user, then start purchase
174 | // flow for subscription.
175 | public void onSubscribeButtonClicked(View arg0) {
176 | if (!mHelper.subscriptionsSupported()) {
177 | complain("Subscriptions not supported on your device yet. Sorry!");
178 | return;
179 | }
180 |
181 | CharSequence[] options;
182 | if (!mSubscribedToDelaroy || !mAutoRenewEnabled) {
183 | // Both subscription options should be available
184 | options = new CharSequence[4];
185 | options[0] = getString(R.string.subscription_period_monthly);
186 | options[1] = getString(R.string.subscription_period_threemonth);
187 | options[2] = getString(R.string.subscription_period_sixmonth);
188 | options[3] = getString(R.string.subscription_period_yearly);
189 | mFirstChoiceSku = SKU_DELAROY_MONTHLY;
190 | mSecondChoiceSku = SKU_DELAROY_THREEMONTH;
191 | mThirdChoiceSku = SKU_DELAROY_SIXMONTH;
192 | mFourthChoiceSku = SKU_DELAROY_YEARLY;
193 | } else {
194 | // This is the subscription upgrade/downgrade path, so only one option is valid
195 | options = new CharSequence[3];
196 | if (mDelaroySku.equals(SKU_DELAROY_MONTHLY)) {
197 | // Give the option to upgrade below
198 | options[0] = getString(R.string.subscription_period_threemonth);
199 | options[1] = getString(R.string.subscription_period_sixmonth);
200 | options[2] = getString(R.string.subscription_period_yearly);
201 | mFirstChoiceSku = SKU_DELAROY_THREEMONTH;
202 | mSecondChoiceSku = SKU_DELAROY_SIXMONTH;
203 | mThirdChoiceSku = SKU_DELAROY_YEARLY;
204 | } else if (mDelaroySku.equals(SKU_DELAROY_THREEMONTH)){
205 | // Give the option to upgrade or downgrade below
206 | options[0] = getString(R.string.subscription_period_monthly);
207 | options[1] = getString(R.string.subscription_period_sixmonth);
208 | options[2] = getString(R.string.subscription_period_yearly);
209 | mFirstChoiceSku = SKU_DELAROY_MONTHLY;
210 | mSecondChoiceSku = SKU_DELAROY_SIXMONTH;
211 | mThirdChoiceSku = SKU_DELAROY_YEARLY;
212 | }else if (mDelaroySku.equals(SKU_DELAROY_SIXMONTH)){
213 | // Give the option to upgrade or downgrade below
214 | options[0] = getString(R.string.subscription_period_monthly);
215 | options[1] = getString(R.string.subscription_period_threemonth);
216 | options[2] = getString(R.string.subscription_period_yearly);
217 | mFirstChoiceSku = SKU_DELAROY_MONTHLY;
218 | mSecondChoiceSku = SKU_DELAROY_THREEMONTH;
219 | mThirdChoiceSku = SKU_DELAROY_YEARLY;
220 |
221 | }else{
222 | // Give the option to upgrade or downgrade below
223 | options[0] = getString(R.string.subscription_period_monthly);
224 | options[1] = getString(R.string.subscription_period_threemonth);
225 | options[2] = getString(R.string.subscription_period_sixmonth);
226 | mFirstChoiceSku = SKU_DELAROY_THREEMONTH;
227 | mSecondChoiceSku = SKU_DELAROY_SIXMONTH;
228 | mThirdChoiceSku = SKU_DELAROY_YEARLY;
229 | }
230 | mFourthChoiceSku = "";
231 | }
232 |
233 | int titleResId;
234 | if (!mSubscribedToDelaroy) {
235 | titleResId = R.string.subscription_period_prompt;
236 | } else if (!mAutoRenewEnabled) {
237 | titleResId = R.string.subscription_resignup_prompt;
238 | } else {
239 | titleResId = R.string.subscription_update_prompt;
240 | }
241 |
242 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
243 | builder.setTitle(titleResId)
244 | .setSingleChoiceItems(options, 0 /* checkedItem */, this)
245 | .setPositiveButton(R.string.subscription_prompt_continue, this)
246 | .setNegativeButton(R.string.subscription_prompt_cancel, this);
247 | AlertDialog dialog = builder.create();
248 | dialog.show();
249 | }
250 |
251 | @Override
252 | public void onClick(DialogInterface dialog, int id) {
253 | if (id == 0 /* First choice item */) {
254 | mSelectedSubscriptionPeriod = mFirstChoiceSku;
255 | } else if (id == 1 /* Second choice item */) {
256 | mSelectedSubscriptionPeriod = mSecondChoiceSku;
257 | }else if (id == 2) {
258 | mSelectedSubscriptionPeriod = mThirdChoiceSku;
259 | }else if (id == 3){
260 | mSelectedSubscriptionPeriod = mFourthChoiceSku;
261 | } else if (id == DialogInterface.BUTTON_POSITIVE /* continue button */) {
262 |
263 | String payload = "";
264 |
265 | if (TextUtils.isEmpty(mSelectedSubscriptionPeriod)) {
266 | // The user has not changed from the default selection
267 | mSelectedSubscriptionPeriod = mFirstChoiceSku;
268 | }
269 |
270 | List oldSkus = null;
271 | if (!TextUtils.isEmpty(mDelaroySku)
272 | && !mDelaroySku.equals(mSelectedSubscriptionPeriod)) {
273 | // The user currently has a valid subscription, any purchase action is going to
274 | // replace that subscription
275 | oldSkus = new ArrayList();
276 | oldSkus.add(mDelaroySku);
277 | }
278 |
279 | setWaitScreen(true);
280 | try {
281 | mHelper.launchPurchaseFlow(this, mSelectedSubscriptionPeriod, IabHelper.ITEM_TYPE_SUBS,
282 | oldSkus, RC_REQUEST, mPurchaseFinishedListener, payload);
283 | } catch (IabHelper.IabAsyncInProgressException e) {
284 | complain("Error launching purchase flow. Another async operation in progress.");
285 | setWaitScreen(false);
286 | }
287 | // Reset the dialog options
288 | mSelectedSubscriptionPeriod = "";
289 | mFirstChoiceSku = "";
290 | mSecondChoiceSku = "";
291 | } else if (id != DialogInterface.BUTTON_NEGATIVE) {
292 | // There are only four buttons, this should not happen
293 | Log.e(TAG, "Unknown button clicked in subscription dialog: " + id);
294 | }
295 | }
296 |
297 | @Override
298 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
299 | Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);
300 | if (mHelper == null) return;
301 |
302 | // Pass on the activity result to the helper for handling
303 | if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
304 | // not handled, so handle it ourselves (here's where you'd
305 | // perform any handling of activity results not related to in-app
306 | // billing...
307 | super.onActivityResult(requestCode, resultCode, data);
308 | }
309 | else {
310 | Log.d(TAG, "onActivityResult handled by IABUtil.");
311 | }
312 | }
313 |
314 | /** Verifies the developer payload of a purchase. */
315 | boolean verifyDeveloperPayload(Purchase p) {
316 | String payload = p.getDeveloperPayload();
317 |
318 |
319 | return true;
320 | }
321 |
322 | // Callback for when a purchase is finished
323 | IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
324 | public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
325 | Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
326 |
327 | // if we were disposed of in the meantime, quit.
328 | if (mHelper == null) return;
329 |
330 | if (result.isFailure()) {
331 | complain("Error purchasing: " + result);
332 | setWaitScreen(false);
333 | return;
334 | }
335 | if (!verifyDeveloperPayload(purchase)) {
336 | complain("Error purchasing. Authenticity verification failed.");
337 | setWaitScreen(false);
338 | return;
339 | }
340 |
341 | Log.d(TAG, "Purchase successful.");
342 |
343 | if (purchase.getSku().equals(SKU_DELAROY_MONTHLY)
344 | || purchase.getSku().equals(SKU_DELAROY_THREEMONTH)
345 | || purchase.getSku().equals(SKU_DELAROY_SIXMONTH)
346 | || purchase.getSku().equals(SKU_DELAROY_YEARLY)){
347 | // bought the rasbita subscription
348 | Log.d(TAG, "Delaroy subscription purchased.");
349 | alert("Thank you for subscribing to Delaroy!");
350 | mSubscribedToDelaroy = true;
351 | mAutoRenewEnabled = purchase.isAutoRenewing();
352 | mDelaroySku = purchase.getSku();
353 | updateUi();
354 | setWaitScreen(false);
355 | }
356 | }
357 | };
358 |
359 | // Called when consumption is complete
360 | IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
361 | public void onConsumeFinished(Purchase purchase, IabResult result) {
362 | Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result);
363 |
364 | updateUi();
365 | setWaitScreen(false);
366 | Log.d(TAG, "End consumption flow.");
367 | }
368 | };
369 |
370 |
371 | // We're being destroyed. It's important to dispose of the helper here!
372 | @Override
373 | public void onDestroy() {
374 | super.onDestroy();
375 |
376 | // very important:
377 | if (mBroadcastReceiver != null) {
378 | unregisterReceiver(mBroadcastReceiver);
379 | }
380 |
381 | // very important:
382 | Log.d(TAG, "Destroying helper.");
383 | if (mHelper != null) {
384 | mHelper.disposeWhenFinished();
385 | mHelper = null;
386 | }
387 | }
388 |
389 | // updates UI to reflect model
390 | public void updateUi() {
391 |
392 | ImageView subscribeButton = (ImageView) findViewById(R.id.rasbita_subscribe);
393 | if (mSubscribedToDelaroy) {
394 | Intent intent = new Intent(this, DashboardActivity.class);
395 | startActivity(intent);
396 | finish();
397 | } else {
398 | // The user does not have rabista subscription"
399 | subscribeButton.setImageResource(R.drawable.subscribe);
400 | }
401 |
402 |
403 | }
404 |
405 | // Enables or disables the "please wait" screen.
406 | void setWaitScreen(boolean set) {
407 | findViewById(R.id.screen_main).setVisibility(set ? View.GONE : View.VISIBLE);
408 | }
409 |
410 | void complain(String message) {
411 | Log.e(TAG, "**** Delaroy Error: " + message);
412 | alert("Error: " + message);
413 | }
414 |
415 | void alert(String message) {
416 | AlertDialog.Builder bld = new AlertDialog.Builder(this);
417 | bld.setMessage(message);
418 | bld.setNeutralButton("OK", null);
419 | Log.d(TAG, "Showing alert dialog: " + message);
420 | bld.create().show();
421 | }
422 |
423 |
424 | }
425 |
426 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/IabBroadcastReceiver.java:
--------------------------------------------------------------------------------
1 | /* Copyright (c) 2014 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 com.delaroystudios.inappsubscription.util;
17 |
18 | import android.content.BroadcastReceiver;
19 | import android.content.Context;
20 | import android.content.Intent;
21 |
22 |
23 | public class IabBroadcastReceiver extends BroadcastReceiver {
24 | /**
25 | * Listener interface for received broadcast messages.
26 | */
27 | public interface IabBroadcastListener {
28 | void receivedBroadcast();
29 | }
30 |
31 | /**
32 | * The Intent action that this Receiver should filter for.
33 | */
34 | public static final String ACTION = "com.android.vending.billing.PURCHASES_UPDATED";
35 |
36 | private final IabBroadcastListener mListener;
37 |
38 | public IabBroadcastReceiver(IabBroadcastListener listener) {
39 | mListener = listener;
40 | }
41 |
42 | @Override
43 | public void onReceive(Context context, Intent intent) {
44 | if (mListener != null) {
45 | mListener.receivedBroadcast();
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/IabException.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/IabHelper.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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.content.pm.ResolveInfo;
26 | import android.os.Bundle;
27 | import android.os.Handler;
28 | import android.os.IBinder;
29 | import android.os.RemoteException;
30 | import android.text.TextUtils;
31 | import android.util.Log;
32 |
33 | import com.android.vending.billing.IInAppBillingService;
34 |
35 | import org.json.JSONException;
36 |
37 | import java.util.ArrayList;
38 | import java.util.List;
39 |
40 |
41 | /**
42 | * Provides convenience methods for in-app billing. You can create one instance of this
43 | * class for your application and use it to process in-app billing operations.
44 | * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
45 | * many common in-app billing operations, as well as automatic signature
46 | * verification.
47 | *
48 | * After instantiating, you must perform setup in order to start using the object.
49 | * To perform setup, call the {@link #startSetup} method and provide a listener;
50 | * that listener will be notified when setup is complete, after which (and not before)
51 | * you may call other methods.
52 | *
53 | * After setup is complete, you will typically want to request an inventory of owned
54 | * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
55 | * and related methods.
56 | *
57 | * When you are done with this object, don't forget to call {@link #dispose}
58 | * to ensure proper cleanup. This object holds a binding to the in-app billing
59 | * service, which will leak unless you dispose of it correctly. If you created
60 | * the object on an Activity's onCreate method, then the recommended
61 | * place to dispose of it is the Activity's onDestroy method. It is invalid to
62 | * dispose the object while an asynchronous operation is in progress. You can
63 | * call {@link #disposeWhenFinished()} to ensure that any in-progress operation
64 | * completes before the object is disposed.
65 | *
66 | * A note about threading: When using this object from a background thread, you may
67 | * call the blocking versions of methods; when using from a UI thread, call
68 | * only the asynchronous versions and handle the results via callbacks.
69 | * Also, notice that you can only call one asynchronous operation at a time;
70 | * attempting to start a second asynchronous operation while the first one
71 | * has not yet completed will result in an exception being thrown.
72 | *
73 | */
74 | public class IabHelper {
75 | // Is debug logging enabled?
76 | boolean mDebugLog = false;
77 | String mDebugTag = "IabHelper";
78 |
79 | // Is setup done?
80 | boolean mSetupDone = false;
81 |
82 | // Has this object been disposed of? (If so, we should ignore callbacks, etc)
83 | boolean mDisposed = false;
84 |
85 | // Do we need to dispose this object after an in-progress asynchronous operation?
86 | boolean mDisposeAfterAsync = false;
87 |
88 | // Are subscriptions supported?
89 | boolean mSubscriptionsSupported = false;
90 |
91 | // Is subscription update supported?
92 | boolean mSubscriptionUpdateSupported = false;
93 |
94 | // Is an asynchronous operation in progress?
95 | // (only one at a time can be in progress)
96 | boolean mAsyncInProgress = false;
97 |
98 | // Ensure atomic access to mAsyncInProgress and mDisposeAfterAsync.
99 | private final Object mAsyncInProgressLock = new Object();
100 |
101 | // (for logging/debugging)
102 | // if mAsyncInProgress == true, what asynchronous operation is in progress?
103 | String mAsyncOperation = "";
104 |
105 | // Context we were passed during initialization
106 | Context mContext;
107 |
108 | // Connection to the service
109 | IInAppBillingService mService;
110 | ServiceConnection mServiceConn;
111 |
112 | // The request code used to launch purchase flow
113 | int mRequestCode;
114 |
115 | // The item type of the current purchase flow
116 | String mPurchasingItemType;
117 |
118 | // Public key for verifying signature, in base64 encoding
119 | String mSignatureBase64 = null;
120 |
121 | // Billing response codes
122 | public static final int BILLING_RESPONSE_RESULT_OK = 0;
123 | public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
124 | public static final int BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE = 2;
125 | public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
126 | public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
127 | public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
128 | public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
129 | public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
130 | public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
131 |
132 | // IAB Helper error codes
133 | public static final int IABHELPER_ERROR_BASE = -1000;
134 | public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
135 | public static final int IABHELPER_BAD_RESPONSE = -1002;
136 | public static final int IABHELPER_VERIFICATION_FAILED = -1003;
137 | public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
138 | public static final int IABHELPER_USER_CANCELLED = -1005;
139 | public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
140 | public static final int IABHELPER_MISSING_TOKEN = -1007;
141 | public static final int IABHELPER_UNKNOWN_ERROR = -1008;
142 | public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
143 | public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
144 | public static final int IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = -1011;
145 |
146 | // Keys for the responses from InAppBillingService
147 | public static final String RESPONSE_CODE = "RESPONSE_CODE";
148 | public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
149 | public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
150 | public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
151 | public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
152 | public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
153 | public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
154 | public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
155 | public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
156 |
157 | // Item types
158 | public static final String ITEM_TYPE_INAPP = "inapp";
159 | public static final String ITEM_TYPE_SUBS = "subs";
160 |
161 | // some fields on the getSkuDetails response bundle
162 | public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
163 | public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
164 |
165 | /**
166 | * Creates an instance. After creation, it will not yet be ready to use. You must perform
167 | * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
168 | * block and is safe to call from a UI thread.
169 | *
170 | * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
171 | * @param base64PublicKey Your application's public key, encoded in base64.
172 | * This is used for verification of purchase signatures. You can find your app's base64-encoded
173 | * public key in your application's page on Google Play Developer Console. Note that this
174 | * is NOT your "developer public key".
175 | */
176 | public IabHelper(Context ctx, String base64PublicKey) {
177 | mContext = ctx.getApplicationContext();
178 | mSignatureBase64 = base64PublicKey;
179 | logDebug("IAB helper created.");
180 | }
181 |
182 | /**
183 | * Enables or disable debug logging through LogCat.
184 | */
185 | public void enableDebugLogging(boolean enable, String tag) {
186 | checkNotDisposed();
187 | mDebugLog = enable;
188 | mDebugTag = tag;
189 | }
190 |
191 | public void enableDebugLogging(boolean enable) {
192 | checkNotDisposed();
193 | mDebugLog = enable;
194 | }
195 |
196 | /**
197 | * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
198 | * when the setup process is complete.
199 | */
200 | public interface OnIabSetupFinishedListener {
201 | /**
202 | * Called to notify that setup is complete.
203 | *
204 | * @param result The result of the setup process.
205 | */
206 | void onIabSetupFinished(IabResult result);
207 | }
208 |
209 | /**
210 | * Starts the setup process. This will start up the setup process asynchronously.
211 | * You will be notified through the listener when the setup process is complete.
212 | * This method is safe to call from a UI thread.
213 | *
214 | * @param listener The listener to notify when the setup process is complete.
215 | */
216 | public void startSetup(final OnIabSetupFinishedListener listener) {
217 | // If already set up, can't do it again.
218 | checkNotDisposed();
219 | if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
220 |
221 | // Connection to IAB service
222 | logDebug("Starting in-app billing setup.");
223 | mServiceConn = new ServiceConnection() {
224 | @Override
225 | public void onServiceDisconnected(ComponentName name) {
226 | logDebug("Billing service disconnected.");
227 | mService = null;
228 | }
229 |
230 | @Override
231 | public void onServiceConnected(ComponentName name, IBinder service) {
232 | if (mDisposed) return;
233 | logDebug("Billing service connected.");
234 | mService = IInAppBillingService.Stub.asInterface(service);
235 | String packageName = mContext.getPackageName();
236 | try {
237 | logDebug("Checking for in-app billing 3 support.");
238 |
239 | // check for in-app billing v3 support
240 | int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
241 | if (response != BILLING_RESPONSE_RESULT_OK) {
242 | if (listener != null) listener.onIabSetupFinished(new IabResult(response,
243 | "Error checking for billing v3 support."));
244 |
245 | // if in-app purchases aren't supported, neither are subscriptions
246 | mSubscriptionsSupported = false;
247 | mSubscriptionUpdateSupported = false;
248 | return;
249 | } else {
250 | logDebug("In-app billing version 3 supported for " + packageName);
251 | }
252 |
253 | // Check for v5 subscriptions support. This is needed for
254 | // getBuyIntentToReplaceSku which allows for subscription update
255 | response = mService.isBillingSupported(5, packageName, ITEM_TYPE_SUBS);
256 | if (response == BILLING_RESPONSE_RESULT_OK) {
257 | logDebug("Subscription re-signup AVAILABLE.");
258 | mSubscriptionUpdateSupported = true;
259 | } else {
260 | logDebug("Subscription re-signup not available.");
261 | mSubscriptionUpdateSupported = false;
262 | }
263 |
264 | if (mSubscriptionUpdateSupported) {
265 | mSubscriptionsSupported = true;
266 | } else {
267 | // check for v3 subscriptions support
268 | response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
269 | if (response == BILLING_RESPONSE_RESULT_OK) {
270 | logDebug("Subscriptions AVAILABLE.");
271 | mSubscriptionsSupported = true;
272 | } else {
273 | logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
274 | mSubscriptionsSupported = false;
275 | mSubscriptionUpdateSupported = false;
276 | }
277 | }
278 |
279 | mSetupDone = true;
280 | }
281 | catch (RemoteException e) {
282 | if (listener != null) {
283 | listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
284 | "RemoteException while setting up in-app billing."));
285 | }
286 | e.printStackTrace();
287 | return;
288 | }
289 |
290 | if (listener != null) {
291 | listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
292 | }
293 | }
294 | };
295 |
296 | Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
297 | serviceIntent.setPackage("com.android.vending");
298 | List intentServices = mContext.getPackageManager().queryIntentServices(serviceIntent, 0);
299 | if (intentServices != null && !intentServices.isEmpty()) {
300 | // service available to handle that Intent
301 | mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
302 | }
303 | else {
304 | // no service available to handle that Intent
305 | if (listener != null) {
306 | listener.onIabSetupFinished(
307 | new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
308 | "Billing service unavailable on device."));
309 | }
310 | }
311 | }
312 |
313 | /**
314 | * Dispose of object, releasing resources. It's very important to call this
315 | * method when you are done with this object. It will release any resources
316 | * used by it such as service connections. Naturally, once the object is
317 | * disposed of, it can't be used again.
318 | */
319 | public void dispose() throws IabAsyncInProgressException {
320 | synchronized (mAsyncInProgressLock) {
321 | if (mAsyncInProgress) {
322 | throw new IabAsyncInProgressException("Can't dispose because an async operation " +
323 | "(" + mAsyncOperation + ") is in progress.");
324 | }
325 | }
326 | logDebug("Disposing.");
327 | mSetupDone = false;
328 | if (mServiceConn != null) {
329 | logDebug("Unbinding from service.");
330 | if (mContext != null) mContext.unbindService(mServiceConn);
331 | }
332 | mDisposed = true;
333 | mContext = null;
334 | mServiceConn = null;
335 | mService = null;
336 | mPurchaseListener = null;
337 | }
338 |
339 | /**
340 | * Disposes of object, releasing resources. If there is an in-progress async operation, this
341 | * method will queue the dispose to occur after the operation has finished.
342 | */
343 | public void disposeWhenFinished() {
344 | synchronized (mAsyncInProgressLock) {
345 | if (mAsyncInProgress) {
346 | logDebug("Will dispose after async operation finishes.");
347 | mDisposeAfterAsync = true;
348 | } else {
349 | try {
350 | dispose();
351 | } catch (IabAsyncInProgressException e) {
352 | // Should never be thrown, because we call dispose() only after checking that
353 | // there's not already an async operation in progress.
354 | }
355 | }
356 | }
357 | }
358 |
359 | private void checkNotDisposed() {
360 | if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
361 | }
362 |
363 | /** Returns whether subscriptions are supported. */
364 | public boolean subscriptionsSupported() {
365 | checkNotDisposed();
366 | return mSubscriptionsSupported;
367 | }
368 |
369 |
370 | /**
371 | * Callback that notifies when a purchase is finished.
372 | */
373 | public interface OnIabPurchaseFinishedListener {
374 | /**
375 | * Called to notify that an in-app purchase finished. If the purchase was successful,
376 | * then the sku parameter specifies which item was purchased. If the purchase failed,
377 | * the sku and extraData parameters may or may not be null, depending on how far the purchase
378 | * process went.
379 | *
380 | * @param result The result of the purchase.
381 | * @param info The purchase information (null if purchase failed)
382 | */
383 | void onIabPurchaseFinished(IabResult result, Purchase info);
384 | }
385 |
386 | // The listener registered on launchPurchaseFlow, which we have to call back when
387 | // the purchase finishes
388 | OnIabPurchaseFinishedListener mPurchaseListener;
389 |
390 | public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener)
391 | throws IabAsyncInProgressException {
392 | launchPurchaseFlow(act, sku, requestCode, listener, "");
393 | }
394 |
395 | public void launchPurchaseFlow(Activity act, String sku, int requestCode,
396 | OnIabPurchaseFinishedListener listener, String extraData)
397 | throws IabAsyncInProgressException {
398 | launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, null, requestCode, listener, extraData);
399 | }
400 |
401 | public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
402 | OnIabPurchaseFinishedListener listener) throws IabAsyncInProgressException {
403 | launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
404 | }
405 |
406 | public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
407 | OnIabPurchaseFinishedListener listener, String extraData)
408 | throws IabAsyncInProgressException {
409 | launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, null, requestCode, listener, extraData);
410 | }
411 |
412 | /**
413 | * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
414 | * which will involve bringing up the Google Play screen. The calling activity will be paused
415 | * while the user interacts with Google Play, and the result will be delivered via the
416 | * activity's {@link android.app.Activity#onActivityResult} method, at which point you must call
417 | * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
418 | * MUST be called from the UI thread of the Activity.
419 | *
420 | * @param act The calling activity.
421 | * @param sku The sku of the item to purchase.
422 | * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or
423 | * ITEM_TYPE_SUBS)
424 | * @param oldSkus A list of SKUs which the new SKU is replacing or null if there are none
425 | * @param requestCode A request code (to differentiate from other responses -- as in
426 | * {@link android.app.Activity#startActivityForResult}).
427 | * @param listener The listener to notify when the purchase process finishes
428 | * @param extraData Extra data (developer payload), which will be returned with the purchase
429 | * data when the purchase completes. This extra data will be permanently bound to that
430 | * purchase and will always be returned when the purchase is queried.
431 | */
432 | public void launchPurchaseFlow(Activity act, String sku, String itemType, List oldSkus,
433 | int requestCode, OnIabPurchaseFinishedListener listener, String extraData)
434 | throws IabAsyncInProgressException {
435 | checkNotDisposed();
436 | checkSetupDone("launchPurchaseFlow");
437 | flagStartAsync("launchPurchaseFlow");
438 | IabResult result;
439 |
440 | if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
441 | IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
442 | "Subscriptions are not available.");
443 | flagEndAsync();
444 | if (listener != null) listener.onIabPurchaseFinished(r, null);
445 | return;
446 | }
447 |
448 | try {
449 | logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
450 | Bundle buyIntentBundle;
451 | if (oldSkus == null || oldSkus.isEmpty()) {
452 | // Purchasing a new item or subscription re-signup
453 | buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType,
454 | extraData);
455 | } else {
456 | // Subscription upgrade/downgrade
457 | if (!mSubscriptionUpdateSupported) {
458 | IabResult r = new IabResult(IABHELPER_SUBSCRIPTION_UPDATE_NOT_AVAILABLE,
459 | "Subscription updates are not available.");
460 | flagEndAsync();
461 | if (listener != null) listener.onIabPurchaseFinished(r, null);
462 | return;
463 | }
464 | buyIntentBundle = mService.getBuyIntentToReplaceSkus(5, mContext.getPackageName(),
465 | oldSkus, sku, itemType, extraData);
466 | }
467 | int response = getResponseCodeFromBundle(buyIntentBundle);
468 | if (response != BILLING_RESPONSE_RESULT_OK) {
469 | logError("Unable to buy item, Error response: " + getResponseDesc(response));
470 | flagEndAsync();
471 | result = new IabResult(response, "Unable to buy item");
472 | if (listener != null) listener.onIabPurchaseFinished(result, null);
473 | return;
474 | }
475 |
476 | PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
477 | logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
478 | mRequestCode = requestCode;
479 | mPurchaseListener = listener;
480 | mPurchasingItemType = itemType;
481 | act.startIntentSenderForResult(pendingIntent.getIntentSender(),
482 | requestCode, new Intent(),
483 | Integer.valueOf(0), Integer.valueOf(0),
484 | Integer.valueOf(0));
485 | }
486 | catch (SendIntentException e) {
487 | logError("SendIntentException while launching purchase flow for sku " + sku);
488 | e.printStackTrace();
489 | flagEndAsync();
490 |
491 | result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
492 | if (listener != null) listener.onIabPurchaseFinished(result, null);
493 | }
494 | catch (RemoteException e) {
495 | logError("RemoteException while launching purchase flow for sku " + sku);
496 | e.printStackTrace();
497 | flagEndAsync();
498 |
499 | result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
500 | if (listener != null) listener.onIabPurchaseFinished(result, null);
501 | }
502 | }
503 |
504 | /**
505 | * Handles an activity result that's part of the purchase flow in in-app billing. If you
506 | * are calling {@link #launchPurchaseFlow}, then you must call this method from your
507 | * Activity's {@link android.app.Activity@onActivityResult} method. This method
508 | * MUST be called from the UI thread of the Activity.
509 | *
510 | * @param requestCode The requestCode as you received it.
511 | * @param resultCode The resultCode as you received it.
512 | * @param data The data (Intent) as you received it.
513 | * @return Returns true if the result was related to a purchase flow and was handled;
514 | * false if the result was not related to a purchase, in which case you should
515 | * handle it normally.
516 | */
517 | public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
518 | IabResult result;
519 | if (requestCode != mRequestCode) return false;
520 |
521 | checkNotDisposed();
522 | checkSetupDone("handleActivityResult");
523 |
524 | // end of async purchase operation that started on launchPurchaseFlow
525 | flagEndAsync();
526 |
527 | if (data == null) {
528 | logError("Null data in IAB activity result.");
529 | result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
530 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
531 | return true;
532 | }
533 |
534 | int responseCode = getResponseCodeFromIntent(data);
535 | String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
536 | String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
537 |
538 | if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
539 | logDebug("Successful resultcode from purchase activity.");
540 | logDebug("Purchase data: " + purchaseData);
541 | logDebug("Data signature: " + dataSignature);
542 | logDebug("Extras: " + data.getExtras());
543 | logDebug("Expected item type: " + mPurchasingItemType);
544 |
545 | if (purchaseData == null || dataSignature == null) {
546 | logError("BUG: either purchaseData or dataSignature is null.");
547 | logDebug("Extras: " + data.getExtras().toString());
548 | result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
549 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
550 | return true;
551 | }
552 |
553 | Purchase purchase = null;
554 | try {
555 | purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
556 | String sku = purchase.getSku();
557 |
558 | // Verify signature
559 | if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
560 | logError("Purchase signature verification FAILED for sku " + sku);
561 | result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
562 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
563 | return true;
564 | }
565 | logDebug("Purchase signature successfully verified.");
566 | }
567 | catch (JSONException e) {
568 | logError("Failed to parse purchase data.");
569 | e.printStackTrace();
570 | result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
571 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
572 | return true;
573 | }
574 |
575 | if (mPurchaseListener != null) {
576 | mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
577 | }
578 | }
579 | else if (resultCode == Activity.RESULT_OK) {
580 | // result code was OK, but in-app billing response was not OK.
581 | logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
582 | if (mPurchaseListener != null) {
583 | result = new IabResult(responseCode, "Problem purchashing item.");
584 | mPurchaseListener.onIabPurchaseFinished(result, null);
585 | }
586 | }
587 | else if (resultCode == Activity.RESULT_CANCELED) {
588 | logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
589 | result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
590 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
591 | }
592 | else {
593 | logError("Purchase failed. Result code: " + Integer.toString(resultCode)
594 | + ". Response: " + getResponseDesc(responseCode));
595 | result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
596 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
597 | }
598 | return true;
599 | }
600 |
601 | public Inventory queryInventory() throws IabException {
602 | return queryInventory(false, null, null);
603 | }
604 |
605 | /**
606 | * Queries the inventory. This will query all owned items from the server, as well as
607 | * information on additional skus, if specified. This method may block or take long to execute.
608 | * Do not call from a UI thread. For that, use the non-blocking version {@link #queryInventoryAsync}.
609 | *
610 | * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
611 | * as purchase information.
612 | * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
613 | * Ignored if null or if querySkuDetails is false.
614 | * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
615 | * Ignored if null or if querySkuDetails is false.
616 | * @throws IabException if a problem occurs while refreshing the inventory.
617 | */
618 | public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus,
619 | List moreSubsSkus) throws IabException {
620 | checkNotDisposed();
621 | checkSetupDone("queryInventory");
622 | try {
623 | Inventory inv = new Inventory();
624 | int r = queryPurchases(inv, ITEM_TYPE_INAPP);
625 | if (r != BILLING_RESPONSE_RESULT_OK) {
626 | throw new IabException(r, "Error refreshing inventory (querying owned items).");
627 | }
628 |
629 | if (querySkuDetails) {
630 | r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
631 | if (r != BILLING_RESPONSE_RESULT_OK) {
632 | throw new IabException(r, "Error refreshing inventory (querying prices of items).");
633 | }
634 | }
635 |
636 | // if subscriptions are supported, then also query for subscriptions
637 | if (mSubscriptionsSupported) {
638 | r = queryPurchases(inv, ITEM_TYPE_SUBS);
639 | if (r != BILLING_RESPONSE_RESULT_OK) {
640 | throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
641 | }
642 |
643 | if (querySkuDetails) {
644 | r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreSubsSkus);
645 | if (r != BILLING_RESPONSE_RESULT_OK) {
646 | throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
647 | }
648 | }
649 | }
650 |
651 | return inv;
652 | }
653 | catch (RemoteException e) {
654 | throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
655 | }
656 | catch (JSONException e) {
657 | throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
658 | }
659 | }
660 |
661 | /**
662 | * Listener that notifies when an inventory query operation completes.
663 | */
664 | public interface QueryInventoryFinishedListener {
665 | /**
666 | * Called to notify that an inventory query operation completed.
667 | *
668 | * @param result The result of the operation.
669 | * @param inv The inventory.
670 | */
671 | void onQueryInventoryFinished(IabResult result, Inventory inv);
672 | }
673 |
674 |
675 | /**
676 | * Asynchronous wrapper for inventory query. This will perform an inventory
677 | * query as described in {@link #queryInventory}, but will do so asynchronously
678 | * and call back the specified listener upon completion. This method is safe to
679 | * call from a UI thread.
680 | *
681 | * @param querySkuDetails as in {@link #queryInventory}
682 | * @param moreItemSkus as in {@link #queryInventory}
683 | * @param moreSubsSkus as in {@link #queryInventory}
684 | * @param listener The listener to notify when the refresh operation completes.
685 | */
686 | public void queryInventoryAsync(final boolean querySkuDetails, final List moreItemSkus,
687 | final List moreSubsSkus, final QueryInventoryFinishedListener listener)
688 | throws IabAsyncInProgressException {
689 | final Handler handler = new Handler();
690 | checkNotDisposed();
691 | checkSetupDone("queryInventory");
692 | flagStartAsync("refresh inventory");
693 | (new Thread(new Runnable() {
694 | public void run() {
695 | IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
696 | Inventory inv = null;
697 | try {
698 | inv = queryInventory(querySkuDetails, moreItemSkus, moreSubsSkus);
699 | }
700 | catch (IabException ex) {
701 | result = ex.getResult();
702 | }
703 |
704 | flagEndAsync();
705 |
706 | final IabResult result_f = result;
707 | final Inventory inv_f = inv;
708 | if (!mDisposed && listener != null) {
709 | handler.post(new Runnable() {
710 | public void run() {
711 | listener.onQueryInventoryFinished(result_f, inv_f);
712 | }
713 | });
714 | }
715 | }
716 | })).start();
717 | }
718 |
719 | public void queryInventoryAsync(QueryInventoryFinishedListener listener)
720 | throws IabAsyncInProgressException{
721 | queryInventoryAsync(false, null, null, listener);
722 | }
723 |
724 | /**
725 | * Consumes a given in-app product. Consuming can only be done on an item
726 | * that's owned, and as a result of consumption, the user will no longer own it.
727 | * This method may block or take long to return. Do not call from the UI thread.
728 | * For that, see {@link #consumeAsync}.
729 | *
730 | * @param itemInfo The PurchaseInfo that represents the item to consume.
731 | * @throws IabException if there is a problem during consumption.
732 | */
733 | void consume(Purchase itemInfo) throws IabException {
734 | checkNotDisposed();
735 | checkSetupDone("consume");
736 |
737 | if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
738 | throw new IabException(IABHELPER_INVALID_CONSUMPTION,
739 | "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
740 | }
741 |
742 | try {
743 | String token = itemInfo.getToken();
744 | String sku = itemInfo.getSku();
745 | if (token == null || token.equals("")) {
746 | logError("Can't consume "+ sku + ". No token.");
747 | throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
748 | + sku + " " + itemInfo);
749 | }
750 |
751 | logDebug("Consuming sku: " + sku + ", token: " + token);
752 | int response = mService.consumePurchase(3, mContext.getPackageName(), token);
753 | if (response == BILLING_RESPONSE_RESULT_OK) {
754 | logDebug("Successfully consumed sku: " + sku);
755 | }
756 | else {
757 | logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
758 | throw new IabException(response, "Error consuming sku " + sku);
759 | }
760 | }
761 | catch (RemoteException e) {
762 | throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
763 | }
764 | }
765 |
766 | /**
767 | * Callback that notifies when a consumption operation finishes.
768 | */
769 | public interface OnConsumeFinishedListener {
770 | /**
771 | * Called to notify that a consumption has finished.
772 | *
773 | * @param purchase The purchase that was (or was to be) consumed.
774 | * @param result The result of the consumption operation.
775 | */
776 | void onConsumeFinished(Purchase purchase, IabResult result);
777 | }
778 |
779 | /**
780 | * Callback that notifies when a multi-item consumption operation finishes.
781 | */
782 | public interface OnConsumeMultiFinishedListener {
783 | /**
784 | * Called to notify that a consumption of multiple items has finished.
785 | *
786 | * @param purchases The purchases that were (or were to be) consumed.
787 | * @param results The results of each consumption operation, corresponding to each
788 | * sku.
789 | */
790 | void onConsumeMultiFinished(List purchases, List results);
791 | }
792 |
793 | /**
794 | * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
795 | * performs the consumption in the background and notifies completion through
796 | * the provided listener. This method is safe to call from a UI thread.
797 | *
798 | * @param purchase The purchase to be consumed.
799 | * @param listener The listener to notify when the consumption operation finishes.
800 | */
801 | public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener)
802 | throws IabAsyncInProgressException {
803 | checkNotDisposed();
804 | checkSetupDone("consume");
805 | List purchases = new ArrayList();
806 | purchases.add(purchase);
807 | consumeAsyncInternal(purchases, listener, null);
808 | }
809 |
810 | /**
811 | * Same as {@link #consumeAsync}, but for multiple items at once.
812 | * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
813 | * @param listener The listener to notify when the consumption operation finishes.
814 | */
815 | public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener)
816 | throws IabAsyncInProgressException {
817 | checkNotDisposed();
818 | checkSetupDone("consume");
819 | consumeAsyncInternal(purchases, null, listener);
820 | }
821 |
822 | /**
823 | * Returns a human-readable description for the given response code.
824 | *
825 | * @param code The response code
826 | * @return A human-readable string explaining the result code.
827 | * It also includes the result code numerically.
828 | */
829 | public static String getResponseDesc(int code) {
830 | String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
831 | "3:Billing Unavailable/4:Item unavailable/" +
832 | "5:Developer Error/6:Error/7:Item Already Owned/" +
833 | "8:Item not owned").split("/");
834 | String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
835 | "-1002:Bad response received/" +
836 | "-1003:Purchase signature verification failed/" +
837 | "-1004:Send intent failed/" +
838 | "-1005:User cancelled/" +
839 | "-1006:Unknown purchase response/" +
840 | "-1007:Missing token/" +
841 | "-1008:Unknown error/" +
842 | "-1009:Subscriptions not available/" +
843 | "-1010:Invalid consumption attempt").split("/");
844 |
845 | if (code <= IABHELPER_ERROR_BASE) {
846 | int index = IABHELPER_ERROR_BASE - code;
847 | if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
848 | else return String.valueOf(code) + ":Unknown IAB Helper Error";
849 | }
850 | else if (code < 0 || code >= iab_msgs.length)
851 | return String.valueOf(code) + ":Unknown";
852 | else
853 | return iab_msgs[code];
854 | }
855 |
856 |
857 | // Checks that setup was done; if not, throws an exception.
858 | void checkSetupDone(String operation) {
859 | if (!mSetupDone) {
860 | logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
861 | throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
862 | }
863 | }
864 |
865 | // Workaround to bug where sometimes response codes come as Long instead of Integer
866 | int getResponseCodeFromBundle(Bundle b) {
867 | Object o = b.get(RESPONSE_CODE);
868 | if (o == null) {
869 | logDebug("Bundle with null response code, assuming OK (known issue)");
870 | return BILLING_RESPONSE_RESULT_OK;
871 | }
872 | else if (o instanceof Integer) return ((Integer)o).intValue();
873 | else if (o instanceof Long) return (int)((Long)o).longValue();
874 | else {
875 | logError("Unexpected type for bundle response code.");
876 | logError(o.getClass().getName());
877 | throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
878 | }
879 | }
880 |
881 | // Workaround to bug where sometimes response codes come as Long instead of Integer
882 | int getResponseCodeFromIntent(Intent i) {
883 | Object o = i.getExtras().get(RESPONSE_CODE);
884 | if (o == null) {
885 | logError("Intent with no response code, assuming OK (known issue)");
886 | return BILLING_RESPONSE_RESULT_OK;
887 | }
888 | else if (o instanceof Integer) return ((Integer)o).intValue();
889 | else if (o instanceof Long) return (int)((Long)o).longValue();
890 | else {
891 | logError("Unexpected type for intent response code.");
892 | logError(o.getClass().getName());
893 | throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
894 | }
895 | }
896 |
897 | void flagStartAsync(String operation) throws IabAsyncInProgressException {
898 | synchronized (mAsyncInProgressLock) {
899 | if (mAsyncInProgress) {
900 | throw new IabAsyncInProgressException("Can't start async operation (" +
901 | operation + ") because another async operation (" + mAsyncOperation +
902 | ") is in progress.");
903 | }
904 | mAsyncOperation = operation;
905 | mAsyncInProgress = true;
906 | logDebug("Starting async operation: " + operation);
907 | }
908 | }
909 |
910 | void flagEndAsync() {
911 | synchronized (mAsyncInProgressLock) {
912 | logDebug("Ending async operation: " + mAsyncOperation);
913 | mAsyncOperation = "";
914 | mAsyncInProgress = false;
915 | if (mDisposeAfterAsync) {
916 | try {
917 | dispose();
918 | } catch (IabAsyncInProgressException e) {
919 | // Should not be thrown, because we reset mAsyncInProgress immediately before
920 | // calling dispose().
921 | }
922 | }
923 | }
924 | }
925 |
926 | /**
927 | * Exception thrown when the requested operation cannot be started because an async operation
928 | * is still in progress.
929 | */
930 | public static class IabAsyncInProgressException extends Exception {
931 | public IabAsyncInProgressException(String message) {
932 | super(message);
933 | }
934 | }
935 |
936 | int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
937 | // Query purchases
938 | logDebug("Querying owned items, item type: " + itemType);
939 | logDebug("Package name: " + mContext.getPackageName());
940 | boolean verificationFailed = false;
941 | String continueToken = null;
942 |
943 | do {
944 | logDebug("Calling getPurchases with continuation token: " + continueToken);
945 | Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
946 | itemType, continueToken);
947 |
948 | int response = getResponseCodeFromBundle(ownedItems);
949 | logDebug("Owned items response: " + String.valueOf(response));
950 | if (response != BILLING_RESPONSE_RESULT_OK) {
951 | logDebug("getPurchases() failed: " + getResponseDesc(response));
952 | return response;
953 | }
954 | if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
955 | || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
956 | || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
957 | logError("Bundle returned from getPurchases() doesn't contain required fields.");
958 | return IABHELPER_BAD_RESPONSE;
959 | }
960 |
961 | ArrayList ownedSkus = ownedItems.getStringArrayList(
962 | RESPONSE_INAPP_ITEM_LIST);
963 | ArrayList purchaseDataList = ownedItems.getStringArrayList(
964 | RESPONSE_INAPP_PURCHASE_DATA_LIST);
965 | ArrayList signatureList = ownedItems.getStringArrayList(
966 | RESPONSE_INAPP_SIGNATURE_LIST);
967 |
968 | for (int i = 0; i < purchaseDataList.size(); ++i) {
969 | String purchaseData = purchaseDataList.get(i);
970 | String signature = signatureList.get(i);
971 | String sku = ownedSkus.get(i);
972 | if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
973 | logDebug("Sku is owned: " + sku);
974 | Purchase purchase = new Purchase(itemType, purchaseData, signature);
975 |
976 | if (TextUtils.isEmpty(purchase.getToken())) {
977 | logWarn("BUG: empty/null token!");
978 | logDebug("Purchase data: " + purchaseData);
979 | }
980 |
981 | // Record ownership and token
982 | inv.addPurchase(purchase);
983 | }
984 | else {
985 | logWarn("Purchase signature verification **FAILED**. Not adding item.");
986 | logDebug(" Purchase data: " + purchaseData);
987 | logDebug(" Signature: " + signature);
988 | verificationFailed = true;
989 | }
990 | }
991 |
992 | continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
993 | logDebug("Continuation token: " + continueToken);
994 | } while (!TextUtils.isEmpty(continueToken));
995 |
996 | return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
997 | }
998 |
999 | int querySkuDetails(String itemType, Inventory inv, List moreSkus)
1000 | throws RemoteException, JSONException {
1001 | logDebug("Querying SKU details.");
1002 | ArrayList skuList = new ArrayList();
1003 | skuList.addAll(inv.getAllOwnedSkus(itemType));
1004 | if (moreSkus != null) {
1005 | for (String sku : moreSkus) {
1006 | if (!skuList.contains(sku)) {
1007 | skuList.add(sku);
1008 | }
1009 | }
1010 | }
1011 |
1012 | if (skuList.size() == 0) {
1013 | logDebug("queryPrices: nothing to do because there are no SKUs.");
1014 | return BILLING_RESPONSE_RESULT_OK;
1015 | }
1016 |
1017 | // Split the sku list in blocks of no more than 20 elements.
1018 | ArrayList> packs = new ArrayList>();
1019 | ArrayList tempList;
1020 | int n = skuList.size() / 20;
1021 | int mod = skuList.size() % 20;
1022 | for (int i = 0; i < n; i++) {
1023 | tempList = new ArrayList();
1024 | for (String s : skuList.subList(i * 20, i * 20 + 20)) {
1025 | tempList.add(s);
1026 | }
1027 | packs.add(tempList);
1028 | }
1029 | if (mod != 0) {
1030 | tempList = new ArrayList();
1031 | for (String s : skuList.subList(n * 20, n * 20 + mod)) {
1032 | tempList.add(s);
1033 | }
1034 | packs.add(tempList);
1035 | }
1036 |
1037 | for (ArrayList skuPartList : packs) {
1038 | Bundle querySkus = new Bundle();
1039 | querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuPartList);
1040 | Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
1041 | itemType, querySkus);
1042 |
1043 | if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
1044 | int response = getResponseCodeFromBundle(skuDetails);
1045 | if (response != BILLING_RESPONSE_RESULT_OK) {
1046 | logDebug("getSkuDetails() failed: " + getResponseDesc(response));
1047 | return response;
1048 | } else {
1049 | logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
1050 | return IABHELPER_BAD_RESPONSE;
1051 | }
1052 | }
1053 |
1054 | ArrayList responseList = skuDetails.getStringArrayList(
1055 | RESPONSE_GET_SKU_DETAILS_LIST);
1056 |
1057 | for (String thisResponse : responseList) {
1058 | SkuDetails d = new SkuDetails(itemType, thisResponse);
1059 | logDebug("Got sku details: " + d);
1060 | inv.addSkuDetails(d);
1061 | }
1062 | }
1063 |
1064 | return BILLING_RESPONSE_RESULT_OK;
1065 | }
1066 |
1067 | void consumeAsyncInternal(final List purchases,
1068 | final OnConsumeFinishedListener singleListener,
1069 | final OnConsumeMultiFinishedListener multiListener)
1070 | throws IabAsyncInProgressException {
1071 | final Handler handler = new Handler();
1072 | flagStartAsync("consume");
1073 | (new Thread(new Runnable() {
1074 | public void run() {
1075 | final List results = new ArrayList();
1076 | for (Purchase purchase : purchases) {
1077 | try {
1078 | consume(purchase);
1079 | results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
1080 | }
1081 | catch (IabException ex) {
1082 | results.add(ex.getResult());
1083 | }
1084 | }
1085 |
1086 | flagEndAsync();
1087 | if (!mDisposed && singleListener != null) {
1088 | handler.post(new Runnable() {
1089 | public void run() {
1090 | singleListener.onConsumeFinished(purchases.get(0), results.get(0));
1091 | }
1092 | });
1093 | }
1094 | if (!mDisposed && multiListener != null) {
1095 | handler.post(new Runnable() {
1096 | public void run() {
1097 | multiListener.onConsumeMultiFinished(purchases, results);
1098 | }
1099 | });
1100 | }
1101 | }
1102 | })).start();
1103 | }
1104 |
1105 | void logDebug(String msg) {
1106 | if (mDebugLog) Log.d(mDebugTag, msg);
1107 | }
1108 |
1109 | void logError(String msg) {
1110 | Log.e(mDebugTag, "In-app billing error: " + msg);
1111 | }
1112 |
1113 | void logWarn(String msg) {
1114 | Log.w(mDebugTag, "In-app billing warning: " + msg);
1115 | }
1116 | }
1117 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/IabResult.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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 |
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/Inventory.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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 mSkuMap = new HashMap();
29 | Map mPurchaseMap = new HashMap();
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 getAllOwnedSkus() {
67 | return new ArrayList(mPurchaseMap.keySet());
68 | }
69 |
70 | /** Returns a list of all owned product IDs of a given type */
71 | List getAllOwnedSkus(String itemType) {
72 | List result = new ArrayList();
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 getAllPurchases() {
81 | return new ArrayList(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 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/Purchase.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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 | boolean mIsAutoRenewing;
36 |
37 | public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
38 | mItemType = itemType;
39 | mOriginalJson = jsonPurchaseInfo;
40 | JSONObject o = new JSONObject(mOriginalJson);
41 | mOrderId = o.optString("orderId");
42 | mPackageName = o.optString("packageName");
43 | mSku = o.optString("productId");
44 | mPurchaseTime = o.optLong("purchaseTime");
45 | mPurchaseState = o.optInt("purchaseState");
46 | mDeveloperPayload = o.optString("developerPayload");
47 | mToken = o.optString("token", o.optString("purchaseToken"));
48 | mIsAutoRenewing = o.optBoolean("autoRenewing");
49 | mSignature = signature;
50 | }
51 |
52 | public String getItemType() { return mItemType; }
53 | public String getOrderId() { return mOrderId; }
54 | public String getPackageName() { return mPackageName; }
55 | public String getSku() { return mSku; }
56 | public long getPurchaseTime() { return mPurchaseTime; }
57 | public int getPurchaseState() { return mPurchaseState; }
58 | public String getDeveloperPayload() { return mDeveloperPayload; }
59 | public String getToken() { return mToken; }
60 | public String getOriginalJson() { return mOriginalJson; }
61 | public String getSignature() { return mSignature; }
62 | public boolean isAutoRenewing() { return mIsAutoRenewing; }
63 |
64 | @Override
65 | public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/Security.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
17 |
18 | import android.text.TextUtils;
19 | import android.util.Base64;
20 | import android.util.Log;
21 |
22 | import java.security.InvalidKeyException;
23 | import java.security.KeyFactory;
24 | import java.security.NoSuchAlgorithmException;
25 | import java.security.PublicKey;
26 | import java.security.Signature;
27 | import java.security.SignatureException;
28 | import java.security.spec.InvalidKeySpecException;
29 | import java.security.spec.X509EncodedKeySpec;
30 |
31 | /**
32 | * Security-related methods. For a secure implementation, all of this code
33 | * should be implemented on a server that communicates with the
34 | * application on the device. For the sake of simplicity and clarity of this
35 | * example, this code is included here and is executed on the device. If you
36 | * must verify the purchases on the phone, you should obfuscate this code to
37 | * make it harder for an attacker to replace the code with stubs that treat all
38 | * purchases as verified.
39 | */
40 | public class Security {
41 | private static final String TAG = "IABUtil/Security";
42 |
43 | private static final String KEY_FACTORY_ALGORITHM = "RSA";
44 | private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
45 |
46 | /**
47 | * Verifies that the data was signed with the given signature, and returns
48 | * the verified purchase. The data is in JSON format and signed
49 | * and product ID of the purchase.
50 | * @param base64PublicKey the base64-encoded public key to use for verifying.
51 | * @param signedData the signed JSON string (signed, not encrypted)
52 | * @param signature the signature for the data, signed with the private key
53 | */
54 | public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
55 | if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
56 | TextUtils.isEmpty(signature)) {
57 | Log.e(TAG, "Purchase verification failed: missing data.");
58 | return false;
59 | }
60 |
61 | PublicKey key = Security.generatePublicKey(base64PublicKey);
62 | return Security.verify(key, signedData, signature);
63 | }
64 |
65 | /**
66 | * Generates a PublicKey instance from a string containing the
67 | * Base64-encoded public key.
68 | *
69 | * @param encodedPublicKey Base64-encoded public key
70 | * @throws IllegalArgumentException if encodedPublicKey is invalid
71 | */
72 | public static PublicKey generatePublicKey(String encodedPublicKey) {
73 | try {
74 | byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
75 | KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
76 | return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
77 | } catch (NoSuchAlgorithmException e) {
78 | throw new RuntimeException(e);
79 | } catch (InvalidKeySpecException e) {
80 | Log.e(TAG, "Invalid key specification.");
81 | throw new IllegalArgumentException(e);
82 | }
83 | }
84 |
85 | /**
86 | * Verifies that the signature from the server matches the computed
87 | * signature on the data. Returns true if the data is correctly signed.
88 | *
89 | * @param publicKey public key associated with the developer account
90 | * @param signedData signed data from server
91 | * @param signature server signature
92 | * @return true if the data and signature match
93 | */
94 | public static boolean verify(PublicKey publicKey, String signedData, String signature) {
95 | byte[] signatureBytes;
96 | try {
97 | signatureBytes = Base64.decode(signature, Base64.DEFAULT);
98 | } catch (IllegalArgumentException e) {
99 | Log.e(TAG, "Base64 decoding failed.");
100 | return false;
101 | }
102 | try {
103 | Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
104 | sig.initVerify(publicKey);
105 | sig.update(signedData.getBytes());
106 | if (!sig.verify(signatureBytes)) {
107 | Log.e(TAG, "Signature verification failed.");
108 | return false;
109 | }
110 | return true;
111 | } catch (NoSuchAlgorithmException e) {
112 | Log.e(TAG, "NoSuchAlgorithmException.");
113 | } catch (InvalidKeyException e) {
114 | Log.e(TAG, "Invalid key specification.");
115 | } catch (SignatureException e) {
116 | Log.e(TAG, "Signature exception.");
117 | }
118 | return false;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/app/src/main/java/com/delaroystudios/inappsubscription/util/SkuDetails.java:
--------------------------------------------------------------------------------
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 com.delaroystudios.inappsubscription.util;
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 | private final String mItemType;
26 | private final String mSku;
27 | private final String mType;
28 | private final String mPrice;
29 | private final long mPriceAmountMicros;
30 | private final String mPriceCurrencyCode;
31 | private final String mTitle;
32 | private final String mDescription;
33 | private final String mJson;
34 |
35 | public SkuDetails(String jsonSkuDetails) throws JSONException {
36 | this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
37 | }
38 |
39 | public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
40 | mItemType = itemType;
41 | mJson = jsonSkuDetails;
42 | JSONObject o = new JSONObject(mJson);
43 | mSku = o.optString("productId");
44 | mType = o.optString("type");
45 | mPrice = o.optString("price");
46 | mPriceAmountMicros = o.optLong("price_amount_micros");
47 | mPriceCurrencyCode = o.optString("price_currency_code");
48 | mTitle = o.optString("title");
49 | mDescription = o.optString("description");
50 | }
51 |
52 | public String getSku() { return mSku; }
53 | public String getType() { return mType; }
54 | public String getPrice() { return mPrice; }
55 | public long getPriceAmountMicros() { return mPriceAmountMicros; }
56 | public String getPriceCurrencyCode() { return mPriceCurrencyCode; }
57 | public String getTitle() { return mTitle; }
58 | public String getDescription() { return mDescription; }
59 |
60 | @Override
61 | public String toString() {
62 | return "SkuDetails:" + mJson;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/subscribe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/drawable/subscribe.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/dashboard_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/subscribe_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
23 |
33 |
34 |
44 |
45 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #e0e0e0
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | InappSubscription
3 |
4 | Select your subscription period
5 | To reactivate your subscription, select your subscription period
6 | To change your subscription, select your new subscription period
7 | Monthly $1.99
8 | Three Month $4.99
9 | Six Month $6.99
10 | Yearly $11.99
11 | Continue
12 | Cancel
13 | App loading image
14 | 80\'s Video Game Style Car Logo
15 | Gas gauge indicator
16 | Free or Premium Image Indicator
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/delaroystudios/inappsubscription/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.delaroystudios.inappsubscription;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:2.3.3'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/delaroy/InappSubscription/3a471b287861cb8ce226733db7f63318516a980a/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Nov 20 07:08:28 WAT 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------