├── .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 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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 | --------------------------------------------------------------------------------