Created
January 15, 2014 23:50
-
-
Save gertcuykens/8447116 to your computer and use it in GitHub Desktop.
IAB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Copyright (c) 2012 Google Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.appspot; | |
import android.app.Activity; | |
import android.app.AlertDialog; | |
import android.content.Intent; | |
import android.content.SharedPreferences; | |
import android.os.Bundle; | |
import android.util.Log; | |
import android.view.View; | |
import android.widget.ImageView; | |
import com.appspot.util.IabHelper; | |
import com.appspot.util.IabResult; | |
import com.appspot.util.Inventory; | |
import com.appspot.util.Purchase; | |
/** | |
* Example game using in-app billing version 3. | |
* | |
* Before attempting to run this sample, please read the README file. It | |
* contains important information on how to set up this project. | |
* | |
* All the game-specific logic is implemented here in MainActivity, while the | |
* general-purpose boilerplate that can be reused in any app is provided in the | |
* classes in the util/ subdirectory. When implementing your own application, | |
* you can copy over util/*.java to make use of those utility classes. | |
* | |
* This game is a simple "driving" game where the player can buy gas | |
* and drive. The car has a tank which stores gas. When the player purchases | |
* gas, the tank fills up (1/4 tank at a time). When the player drives, the gas | |
* in the tank diminishes (also 1/4 tank at a time). | |
* | |
* The user can also purchase a "premium upgrade" that gives them a red car | |
* instead of the standard blue one (exciting!). | |
* | |
* The user can also purchase a subscription ("infinite gas") that allows them | |
* to drive without using up any gas while that subscription is active. | |
* | |
* It's important to note the consumption mechanics for each item. | |
* | |
* PREMIUM: the item is purchased and NEVER consumed. So, after the original | |
* purchase, the player will always own that item. The application knows to | |
* display the red car instead of the blue one because it queries whether | |
* the premium "item" is owned or not. | |
* | |
* INFINITE GAS: this is a subscription, and subscriptions can't be consumed. | |
* | |
* GAS: when gas is purchased, the "gas" item is then owned. We consume it | |
* when we apply that item's effects to our app's world, which to us means | |
* filling up 1/4 of the tank. This happens immediately after purchase! | |
* It's at this point (and not when the user drives) that the "gas" | |
* item is CONSUMED. Consumption should always happen when your game | |
* world was safely updated to apply the effect of the purchase. So, | |
* in an example scenario: | |
* | |
* BEFORE: tank at 1/2 | |
* ON PURCHASE: tank at 1/2, "gas" item is owned | |
* IMMEDIATELY: "gas" is consumed, tank goes to 3/4 | |
* AFTER: tank at 3/4, "gas" item NOT owned any more | |
* | |
* Another important point to notice is that it may so happen that | |
* the application crashed (or anything else happened) after the user | |
* purchased the "gas" item, but before it was consumed. That's why, | |
* on startup, we check if we own the "gas" item, and, if so, | |
* we have to apply its effects to our world and consume it. This | |
* is also very important! | |
* | |
* @author Bruno Oliveira (Google) | |
*/ | |
public class MainActivity extends Activity { | |
// Debug tag, for logging | |
static final String TAG = "TrivialDrive"; | |
// Does the user have the premium upgrade? | |
boolean mIsPremium = false; | |
// Does the user have an active subscription to the infinite gas plan? | |
boolean mSubscribedToInfiniteGas = false; | |
// SKUs for our products: the premium upgrade (non-consumable) and gas (consumable) | |
static final String SKU_PREMIUM = "premium"; | |
static final String SKU_GAS = "gas"; | |
// SKU for our subscription (infinite gas) | |
static final String SKU_INFINITE_GAS = "infinite_gas"; | |
// (arbitrary) request code for the purchase flow | |
static final int RC_REQUEST = 10001; | |
// Graphics for the gas gauge | |
static int[] TANK_RES_IDS = { R.drawable.gas0, R.drawable.gas1, R.drawable.gas2, | |
R.drawable.gas3, R.drawable.gas4 }; | |
// How many units (1/4 tank is our unit) fill in the tank. | |
static final int TANK_MAX = 4; | |
// Current amount of gas in tank, in units | |
int mTank; | |
// The helper object | |
IabHelper mHelper; | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setContentView(R.layout.activity_main); | |
// load game data | |
loadData(); | |
/* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY | |
* (that you got from the Google Play developer console). This is not your | |
* developer public key, it's the *app-specific* public key. | |
* | |
* Instead of just storing the entire literal string here embedded in the | |
* program, construct the key at runtime from pieces or | |
* use bit manipulation (for example, XOR with some other string) to hide | |
* the actual key. The key itself is not secret information, but we don't | |
* want to make it easy for an attacker to replace the public key with one | |
* of their own and then fake messages from the server. | |
*/ | |
String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmd7Dfspg7i+u0vOEUlH0XYE44GmxhO2D1Vfy+8HYm35D61sT86xUEFthGWdmKCv07ncOTThC8Ng4CAHeRknU+gcrZbNqRlQXAf3dG3pZ6aYiFeBrEf8bCK3uzP3JkY/6s9E9q1AMgWV3dKYlvSESVEsNfpZBQoPiRjKCIzXKjKTRn2Uzx8i1b1FgyxgZ+AnLhTyAzmA3dMJbzqTf57cGGDPJLOKKjnwyqY8eiL+YOU22YWrybEPSAtBEl2h360Gn5LjhDo9SNvTMBF32Qs/eLw0iTu6F65GtQNV09dwC6DfA+1hbdWoL95P5ID0jafOFz0XuuTQ6FzTbJb/e6gU9qQIDAQAB"; | |
// Some sanity checks to see if the developer (that's you!) really followed the | |
// instructions to run this sample (don't put these checks on your app!) | |
/*if (base64EncodedPublicKey.contains("CONSTRUCT_YOUR")) { | |
throw new RuntimeException("Please put your app's public key in MainActivity.java. See README."); | |
} | |
if (getPackageName().startsWith("com.example")) { | |
throw new RuntimeException("Please change the sample's package name! See README."); | |
}*/ | |
// Create the helper, passing it our context and the public key to verify signatures with | |
Log.d(TAG, "Creating IAB helper."); | |
mHelper = new IabHelper(this, base64EncodedPublicKey); | |
// enable debug logging (for a production application, you should set this to false). | |
mHelper.enableDebugLogging(true); | |
// Start setup. This is asynchronous and the specified listener | |
// will be called once setup completes. | |
Log.d(TAG, "Starting setup."); | |
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { | |
public void onIabSetupFinished(IabResult result) { | |
Log.d(TAG, "Setup finished."); | |
if (!result.isSuccess()) { | |
// Oh noes, there was a problem. | |
complain("Problem setting up in-app billing: " + result); | |
return; | |
} | |
// Have we been disposed of in the meantime? If so, quit. | |
if (mHelper == null) return; | |
// IAB is fully set up. Now, let's get an inventory of stuff we own. | |
Log.d(TAG, "Setup successful. Querying inventory."); | |
mHelper.queryInventoryAsync(mGotInventoryListener); | |
} | |
}); | |
} | |
// Listener that's called when we finish querying the items and subscriptions we own | |
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() { | |
public void onQueryInventoryFinished(IabResult result, Inventory inventory) { | |
Log.d(TAG, "Query inventory finished."); | |
// Have we been disposed of in the meantime? If so, quit. | |
if (mHelper == null) return; | |
// Is it a failure? | |
if (result.isFailure()) { | |
complain("Failed to query inventory: " + result); | |
return; | |
} | |
Log.d(TAG, "Query inventory was successful."); | |
/* | |
* Check for items we own. Notice that for each purchase, we check | |
* the developer payload to see if it's correct! See | |
* verifyDeveloperPayload(). | |
*/ | |
// Do we have the premium upgrade? | |
Purchase premiumPurchase = inventory.getPurchase(SKU_PREMIUM); | |
mIsPremium = (premiumPurchase != null && verifyDeveloperPayload(premiumPurchase)); | |
Log.d(TAG, "User is " + (mIsPremium ? "PREMIUM" : "NOT PREMIUM")); | |
// Do we have the infinite gas plan? | |
Purchase infiniteGasPurchase = inventory.getPurchase(SKU_INFINITE_GAS); | |
mSubscribedToInfiniteGas = (infiniteGasPurchase != null && | |
verifyDeveloperPayload(infiniteGasPurchase)); | |
Log.d(TAG, "User " + (mSubscribedToInfiniteGas ? "HAS" : "DOES NOT HAVE") | |
+ " infinite gas subscription."); | |
if (mSubscribedToInfiniteGas) mTank = TANK_MAX; | |
// Check for gas delivery -- if we own gas, we should fill up the tank immediately | |
Purchase gasPurchase = inventory.getPurchase(SKU_GAS); | |
if (gasPurchase != null && verifyDeveloperPayload(gasPurchase)) { | |
Log.d(TAG, "We have gas. Consuming it."); | |
mHelper.consumeAsync(inventory.getPurchase(SKU_GAS), mConsumeFinishedListener); | |
return; | |
} | |
updateUi(); | |
setWaitScreen(false); | |
Log.d(TAG, "Initial inventory query finished; enabling main UI."); | |
} | |
}; | |
// User clicked the "Buy Gas" button | |
public void onBuyGasButtonClicked(View arg0) { | |
Log.d(TAG, "Buy gas button clicked."); | |
if (mSubscribedToInfiniteGas) { | |
complain("No need! You're subscribed to infinite gas. Isn't that awesome?"); | |
return; | |
} | |
if (mTank >= TANK_MAX) { | |
complain("Your tank is full. Drive around a bit!"); | |
return; | |
} | |
// launch the gas purchase UI flow. | |
// We will be notified of completion via mPurchaseFinishedListener | |
setWaitScreen(true); | |
Log.d(TAG, "Launching purchase flow for gas."); | |
/* TODO: for security, generate your payload here for verification. See the comments on | |
* verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use | |
* an empty string, but on a production app you should carefully generate this. */ | |
String payload = ""; | |
mHelper.launchPurchaseFlow(this, SKU_GAS, RC_REQUEST, | |
mPurchaseFinishedListener, payload); | |
} | |
// User clicked the "Upgrade to Premium" button. | |
public void onUpgradeAppButtonClicked(View arg0) { | |
Log.d(TAG, "Upgrade button clicked; launching purchase flow for upgrade."); | |
setWaitScreen(true); | |
/* TODO: for security, generate your payload here for verification. See the comments on | |
* verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use | |
* an empty string, but on a production app you should carefully generate this. */ | |
String payload = ""; | |
mHelper.launchPurchaseFlow(this, SKU_PREMIUM, RC_REQUEST, | |
mPurchaseFinishedListener, payload); | |
} | |
// "Subscribe to infinite gas" button clicked. Explain to user, then start purchase | |
// flow for subscription. | |
public void onInfiniteGasButtonClicked(View arg0) { | |
if (!mHelper.subscriptionsSupported()) { | |
complain("Subscriptions not supported on your device yet. Sorry!"); | |
return; | |
} | |
/* TODO: for security, generate your payload here for verification. See the comments on | |
* verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use | |
* an empty string, but on a production app you should carefully generate this. */ | |
String payload = ""; | |
setWaitScreen(true); | |
Log.d(TAG, "Launching purchase flow for infinite gas subscription."); | |
mHelper.launchPurchaseFlow(this, | |
SKU_INFINITE_GAS, IabHelper.ITEM_TYPE_SUBS, | |
RC_REQUEST, mPurchaseFinishedListener, payload); | |
} | |
@Override | |
protected void onActivityResult(int requestCode, int resultCode, Intent data) { | |
Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data); | |
if (mHelper == null) return; | |
// Pass on the activity result to the helper for handling | |
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) { | |
// not handled, so handle it ourselves (here's where you'd | |
// perform any handling of activity results not related to in-app | |
// billing... | |
super.onActivityResult(requestCode, resultCode, data); | |
} | |
else { | |
Log.d(TAG, "onActivityResult handled by IABUtil."); | |
} | |
} | |
/** Verifies the developer payload of a purchase. */ | |
boolean verifyDeveloperPayload(Purchase p) { | |
String payload = p.getDeveloperPayload(); | |
/* | |
* TODO: verify that the developer payload of the purchase is correct. It will be | |
* the same one that you sent when initiating the purchase. | |
* | |
* WARNING: Locally generating a random string when starting a purchase and | |
* verifying it here might seem like a good approach, but this will fail in the | |
* case where the user purchases an item on one device and then uses your app on | |
* a different device, because on the other device you will not have access to the | |
* random string you originally generated. | |
* | |
* So a good developer payload has these characteristics: | |
* | |
* 1. If two different users purchase an item, the payload is different between them, | |
* so that one user's purchase can't be replayed to another user. | |
* | |
* 2. The payload must be such that you can verify it even when the app wasn't the | |
* one who initiated the purchase flow (so that items purchased by the user on | |
* one device work on other devices owned by the user). | |
* | |
* Using your own server to store and verify developer payloads across app | |
* installations is recommended. | |
*/ | |
return true; | |
} | |
// Callback for when a purchase is finished | |
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() { | |
public void onIabPurchaseFinished(IabResult result, Purchase purchase) { | |
Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase); | |
// if we were disposed of in the meantime, quit. | |
if (mHelper == null) return; | |
if (result.isFailure()) { | |
complain("Error purchasing: " + result); | |
setWaitScreen(false); | |
return; | |
} | |
if (!verifyDeveloperPayload(purchase)) { | |
complain("Error purchasing. Authenticity verification failed."); | |
setWaitScreen(false); | |
return; | |
} | |
Log.d(TAG, "Purchase successful."); | |
if (purchase.getSku().equals(SKU_GAS)) { | |
// bought 1/4 tank of gas. So consume it. | |
Log.d(TAG, "Purchase is gas. Starting gas consumption."); | |
mHelper.consumeAsync(purchase, mConsumeFinishedListener); | |
} | |
else if (purchase.getSku().equals(SKU_PREMIUM)) { | |
// bought the premium upgrade! | |
Log.d(TAG, "Purchase is premium upgrade. Congratulating user."); | |
alert("Thank you for upgrading to premium!"); | |
mIsPremium = true; | |
updateUi(); | |
setWaitScreen(false); | |
} | |
else if (purchase.getSku().equals(SKU_INFINITE_GAS)) { | |
// bought the infinite gas subscription | |
Log.d(TAG, "Infinite gas subscription purchased."); | |
alert("Thank you for subscribing to infinite gas!"); | |
mSubscribedToInfiniteGas = true; | |
mTank = TANK_MAX; | |
updateUi(); | |
setWaitScreen(false); | |
} | |
} | |
}; | |
// Called when consumption is complete | |
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() { | |
public void onConsumeFinished(Purchase purchase, IabResult result) { | |
Log.d(TAG, "Consumption finished. Purchase: " + purchase + ", result: " + result); | |
// if we were disposed of in the meantime, quit. | |
if (mHelper == null) return; | |
// We know this is the "gas" sku because it's the only one we consume, | |
// so we don't check which sku was consumed. If you have more than one | |
// sku, you probably should check... | |
if (result.isSuccess()) { | |
// successfully consumed, so we apply the effects of the item in our | |
// game world's logic, which in our case means filling the gas tank a bit | |
Log.d(TAG, "Consumption successful. Provisioning."); | |
mTank = mTank == TANK_MAX ? TANK_MAX : mTank + 1; | |
saveData(); | |
alert("You filled 1/4 tank. Your tank is now " + String.valueOf(mTank) + "/4 full!"); | |
} | |
else { | |
complain("Error while consuming: " + result); | |
} | |
updateUi(); | |
setWaitScreen(false); | |
Log.d(TAG, "End consumption flow."); | |
} | |
}; | |
// Drive button clicked. Burn gas! | |
public void onDriveButtonClicked(View arg0) { | |
Log.d(TAG, "Drive button clicked."); | |
if (!mSubscribedToInfiniteGas && mTank <= 0) alert("Oh, no! You are out of gas! Try buying some!"); | |
else { | |
if (!mSubscribedToInfiniteGas) --mTank; | |
saveData(); | |
alert("Vroooom, you drove a few miles."); | |
updateUi(); | |
Log.d(TAG, "Vrooom. Tank is now " + mTank); | |
} | |
} | |
// We're being destroyed. It's important to dispose of the helper here! | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
// very important: | |
Log.d(TAG, "Destroying helper."); | |
if (mHelper != null) { | |
mHelper.dispose(); | |
mHelper = null; | |
} | |
} | |
// updates UI to reflect model | |
public void updateUi() { | |
// update the car color to reflect premium status or lack thereof | |
((ImageView)findViewById(R.id.free_or_premium)).setImageResource(mIsPremium ? R.drawable.premium : R.drawable.free); | |
// "Upgrade" button is only visible if the user is not premium | |
findViewById(R.id.upgrade_button).setVisibility(mIsPremium ? View.GONE : View.VISIBLE); | |
// "Get infinite gas" button is only visible if the user is not subscribed yet | |
findViewById(R.id.infinite_gas_button).setVisibility(mSubscribedToInfiniteGas ? | |
View.GONE : View.VISIBLE); | |
// update gas gauge to reflect tank status | |
if (mSubscribedToInfiniteGas) { | |
((ImageView)findViewById(R.id.gas_gauge)).setImageResource(R.drawable.gas_inf); | |
} | |
else { | |
int index = mTank >= TANK_RES_IDS.length ? TANK_RES_IDS.length - 1 : mTank; | |
((ImageView)findViewById(R.id.gas_gauge)).setImageResource(TANK_RES_IDS[index]); | |
} | |
} | |
// Enables or disables the "please wait" screen. | |
void setWaitScreen(boolean set) { | |
findViewById(R.id.screen_main).setVisibility(set ? View.GONE : View.VISIBLE); | |
findViewById(R.id.screen_wait).setVisibility(set ? View.VISIBLE : View.GONE); | |
} | |
void complain(String message) { | |
Log.e(TAG, "**** TrivialDrive Error: " + message); | |
alert("Error: " + message); | |
} | |
void alert(String message) { | |
AlertDialog.Builder bld = new AlertDialog.Builder(this); | |
bld.setMessage(message); | |
bld.setNeutralButton("OK", null); | |
Log.d(TAG, "Showing alert dialog: " + message); | |
bld.create().show(); | |
} | |
void saveData() { | |
/* | |
* WARNING: on a real application, we recommend you save data in a secure way to | |
* prevent tampering. For simplicity in this sample, we simply store the data using a | |
* SharedPreferences. | |
*/ | |
SharedPreferences.Editor spe = getPreferences(MODE_PRIVATE).edit(); | |
spe.putInt("tank", mTank); | |
spe.commit(); | |
Log.d(TAG, "Saved data: tank = " + String.valueOf(mTank)); | |
} | |
void loadData() { | |
SharedPreferences sp = getPreferences(MODE_PRIVATE); | |
mTank = sp.getInt("tank", 2); | |
Log.d(TAG, "Loaded data: tank = " + String.valueOf(mTank)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment