/*
* Button Clicker
* Sample Implementation of the In-App Purchasing APIs
* � 2012, Amazon.com, Inc. or its affiliates.
* All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
* http://aws.amazon.com/apache2.0/
* or in the "license" file accompanying this file.
* This file 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.amazon.sample.buttonclicker;
import java.util.Date;
import java.util.LinkedList;
import java.util.Map;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.Log;
import com.amazon.inapp.purchasing.BasePurchasingObserver;
import com.amazon.inapp.purchasing.GetUserIdResponse;
import com.amazon.inapp.purchasing.GetUserIdResponse.GetUserIdRequestStatus;
import com.amazon.inapp.purchasing.Item;
import com.amazon.inapp.purchasing.ItemDataResponse;
import com.amazon.inapp.purchasing.Offset;
import com.amazon.inapp.purchasing.PurchaseResponse;
import com.amazon.inapp.purchasing.PurchaseUpdatesResponse;
import com.amazon.inapp.purchasing.PurchasingManager;
import com.amazon.inapp.purchasing.Receipt;
import com.amazon.inapp.purchasing.SubscriptionPeriod;
/**
* Purchasing Observer will be called on by the Purchasing Manager asynchronously.
* Since the methods on the UI thread of the application, all fulfillment logic is done via an AsyncTask. This way, any
* intensive processes will not hang the UI thread and cause the application to become
* unresponsive.
*/
public class ButtonClickerObserver extends BasePurchasingObserver {
private static final String OFFSET = "offset";
private static final String START_DATE = "startDate";
private static final String TAG = "Amazon-IAP";
private final ButtonClickerActivity baseActivity;
/**
* Creates new instance of the ButtonClickerObserver class.
*
* @param buttonClickerActivity Activity context
*/
public ButtonClickerObserver(final ButtonClickerActivity buttonClickerActivity) {
super(buttonClickerActivity);
this.baseActivity = buttonClickerActivity;
}
/**
* Invoked once the observer is registered with the Puchasing Manager If the boolean is false, the application is
* receiving responses from the SDK Tester. If the boolean is true, the application is live in production.
*
* @param isSandboxMode
* Boolean value that shows if the app is live or not.
*/
@Override
public void onSdkAvailable(final boolean isSandboxMode) {
Log.v(TAG, "onSdkAvailable recieved: Response -" + isSandboxMode);
PurchasingManager.initiateGetUserIdRequest();
}
/**
* Invoked once the call from initiateGetUserIdRequest is completed.
* On a successful response, a response object is passed which contains the request id, request status, and the
* userid generated for your application.
*
* @param getUserIdResponse
* Response object containing the UserID
*/
@Override
public void onGetUserIdResponse(final GetUserIdResponse getUserIdResponse) {
Log.v(TAG, "onGetUserIdResponse recieved: Response -" + getUserIdResponse);
Log.v(TAG, "RequestId:" + getUserIdResponse.getRequestId());
Log.v(TAG, "IdRequestStatus:" + getUserIdResponse.getUserIdRequestStatus());
new GetUserIdAsyncTask().execute(getUserIdResponse);
}
/**
* Invoked once the call from initiateItemDataRequest is completed.
* On a successful response, a response object is passed which contains the request id, request status, and a set of
* item data for the requested skus. Items that have been suppressed or are unavailable will be returned in a
* set of unavailable skus.
*
* @param itemDataResponse
* Response object containing a set of purchasable/non-purchasable items
*/
@Override
public void onItemDataResponse(final ItemDataResponse itemDataResponse) {
Log.v(TAG, "onItemDataResponse recieved");
Log.v(TAG, "ItemDataRequestStatus" + itemDataResponse.getItemDataRequestStatus());
Log.v(TAG, "ItemDataRequestId" + itemDataResponse.getRequestId());
new ItemDataAsyncTask().execute(itemDataResponse);
}
/**
* Is invoked once the call from initiatePurchaseRequest is completed.
* On a successful response, a response object is passed which contains the request id, request status, and the
* receipt of the purchase.
*
* @param purchaseResponse
* Response object containing a receipt of a purchase
*/
@Override
public void onPurchaseResponse(final PurchaseResponse purchaseResponse) {
Log.v(TAG, "onPurchaseResponse recieved");
Log.v(TAG, "PurchaseRequestStatus:" + purchaseResponse.getPurchaseRequestStatus());
new PurchaseAsyncTask().execute(purchaseResponse);
}
/**
* Is invoked once the call from initiatePurchaseUpdatesRequest is completed.
* On a successful response, a response object is passed which contains the request id, request status, a set of
* previously purchased receipts, a set of revoked skus, and the next offset if applicable. If a user downloads your
* application to another device, this call is used to sync up this device with all the user's purchases.
*
* @param purchaseUpdatesResponse
* Response object containing the user's recent purchases.
*/
@Override
public void onPurchaseUpdatesResponse(final PurchaseUpdatesResponse purchaseUpdatesResponse) {
Log.v(TAG, "onPurchaseUpdatesRecived recieved: Response -" + purchaseUpdatesResponse);
Log.v(TAG, "PurchaseUpdatesRequestStatus:" + purchaseUpdatesResponse.getPurchaseUpdatesRequestStatus());
Log.v(TAG, "RequestID:" + purchaseUpdatesResponse.getRequestId());
new PurchaseUpdatesAsyncTask().execute(purchaseUpdatesResponse);
}
/*
* Helper method to print out relevant receipt information to the log.
*/
private void printReceipt(final Receipt receipt) {
Log.v(
TAG,
String.format("Receipt: ItemType: %s Sku: %s SubscriptionPeriod: %s", receipt.getItemType(),
receipt.getSku(), receipt.getSubscriptionPeriod()));
}
/*
* Helper method to retrieve the correct key to use with our shared preferences
*/
private String getKey(final String sku) {
if (sku.equals(baseActivity.getResources().getString(R.string.consumable_sku))) {
return ButtonClickerActivity.NUM_CLICKS;
} else if (sku.equals(baseActivity.getResources().getString(R.string.entitlement_sku_blue))) {
return ButtonClickerActivity.BLUE_BUTTON;
} else if (sku.equals(baseActivity.getResources().getString(R.string.entitlement_sku_purple))) {
return ButtonClickerActivity.PURPLE_BUTTON;
} else if (sku.equals(baseActivity.getResources().getString(R.string.entitlement_sku_green))) {
return ButtonClickerActivity.GREEN_BUTTON;
} else if (sku.equals(baseActivity.getResources().getString(R.string.parent_subscription_sku)) ||
sku.equals(baseActivity.getResources().getString(R.string.child_subscription_sku_monthly))) {
return ButtonClickerActivity.HAS_SUBSCRIPTION;
} else {
return "";
}
}
private SharedPreferences getSharedPreferencesForCurrentUser() {
final SharedPreferences settings = baseActivity.getSharedPreferences(baseActivity.getCurrentUser(), Context.MODE_PRIVATE);
return settings;
}
private SharedPreferences.Editor getSharedPreferencesEditor(){
return getSharedPreferencesForCurrentUser().edit();
}
/*
* Started when the Observer receives a GetUserIdResponse. The Shared Preferences file for the returned user id is
* accessed.
*/
private class GetUserIdAsyncTask extends AsyncTask<GetUserIdResponse, Void, Boolean> {
@Override
protected Boolean doInBackground(final GetUserIdResponse... params) {
GetUserIdResponse getUserIdResponse = params[0];
if (getUserIdResponse.getUserIdRequestStatus() == GetUserIdRequestStatus.SUCCESSFUL) {
final String userId = getUserIdResponse.getUserId();
// Each UserID has their own shared preferences file, and we'll load that file when a new user logs in.
baseActivity.setCurrentUser(userId);
return true;
} else {
Log.v(TAG, "onGetUserIdResponse: Unable to get user ID.");
return false;
}
}
/*
* Call initiatePurchaseUpdatesRequest for the returned user to sync purchases that are not yet fulfilled.
*/
@Override
protected void onPostExecute(final Boolean result) {
super.onPostExecute(result);
if (result) {
PurchasingManager.initiatePurchaseUpdatesRequest(Offset.fromString(baseActivity.getApplicationContext()
.getSharedPreferences(baseActivity.getCurrentUser(), Context.MODE_PRIVATE)
.getString(OFFSET, Offset.BEGINNING.toString())));
}
}
}
/*
* Started when the observer receives an Item Data Response.
* Takes the items and display them in the logs. You can use this information to display an in game
* storefront for your IAP items.
*/
private class ItemDataAsyncTask extends AsyncTask<ItemDataResponse, Void, Void> {
@Override
protected Void doInBackground(final ItemDataResponse... params) {
final ItemDataResponse itemDataResponse = params[0];
switch (itemDataResponse.getItemDataRequestStatus()) {
case SUCCESSFUL_WITH_UNAVAILABLE_SKUS:
// Skus that you can not purchase will be here.
for (final String s : itemDataResponse.getUnavailableSkus()) {
Log.v(TAG, "Unavailable SKU:" + s);
}
case SUCCESSFUL:
// Information you'll want to display about your IAP items is here
// In this example we'll simply log them.
final Map<String, Item> items = itemDataResponse.getItemData();
for (final String key : items.keySet()) {
Item i = items.get(key);
Log.v(TAG, String.format("Item: %s\n Type: %s\n SKU: %s\n Price: %s\n Description: %s\n", i.getTitle(), i.getItemType(), i.getSku(), i.getPrice(), i.getDescription()));
}
break;
case FAILED:
// On failed responses will fail gracefully.
break;
}
return null;
}
}
/*
* Started when the observer receives a Purchase Response
* Once the AsyncTask returns successfully, the UI is updated.
*/
private class PurchaseAsyncTask extends AsyncTask<PurchaseResponse, Void, Boolean> {
@Override
protected Boolean doInBackground(final PurchaseResponse... params) {
final PurchaseResponse purchaseResponse = params[0];
final String userId = baseActivity.getCurrentUser();
if (!purchaseResponse.getUserId().equals(userId)) {
// currently logged in user is different than what we have so update the state
baseActivity.setCurrentUser(purchaseResponse.getUserId());
PurchasingManager.initiatePurchaseUpdatesRequest(Offset.fromString(baseActivity.getSharedPreferences(baseActivity.getCurrentUser(), Context.MODE_PRIVATE)
.getString(OFFSET, Offset.BEGINNING.toString())));
}
final SharedPreferences settings = getSharedPreferencesForCurrentUser();
final SharedPreferences.Editor editor = getSharedPreferencesEditor();
switch (purchaseResponse.getPurchaseRequestStatus()) {
case SUCCESSFUL:
/*
* You can verify the receipt and fulfill the purchase on successful responses.
*/
final Receipt receipt = purchaseResponse.getReceipt();
String key = "";
switch (receipt.getItemType()) {
case CONSUMABLE:
int numClicks = settings.getInt(ButtonClickerActivity.NUM_CLICKS, 0);
editor.putInt(ButtonClickerActivity.NUM_CLICKS, numClicks + 10);
break;
case ENTITLED:
key = getKey(receipt.getSku());
editor.putBoolean(key, true);
break;
case SUBSCRIPTION:
key = getKey(receipt.getSku());
editor.putBoolean(key, true);
editor.putLong(START_DATE, new Date().getTime());
break;
}
editor.commit();
printReceipt(purchaseResponse.getReceipt());
return true;
case ALREADY_ENTITLED:
/*
* If the customer has already been entitled to the item, a receipt is not returned.
* Fulfillment is done unconditionally, we determine which item should be fulfilled by matching the
* request id returned from the initial request with the request id stored in the response.
*/
final String requestId = purchaseResponse.getRequestId();
editor.putBoolean(baseActivity.requestIds.get(requestId), true);
editor.commit();
return true;
case FAILED:
/*
* If the purchase failed for some reason, (The customer canceled the order, or some other
* extraneous circumstance happens) the application ignores the request and logs the failure.
*/
Log.v(TAG, "Failed purchase for request" + baseActivity.requestIds.get(purchaseResponse.getRequestId()));
return false;
case INVALID_SKU:
/*
* If the sku that was purchased was invalid, the application ignores the request and logs the failure.
* This can happen when there is a sku mismatch between what is sent from the application and what
* currently exists on the dev portal.
*/
Log.v(TAG, "Invalid Sku for request " + baseActivity.requestIds.get(purchaseResponse.getRequestId()));
return false;
}
return false;
}
@Override
protected void onPostExecute(final Boolean success) {
super.onPostExecute(success);
if (success) {
baseActivity.update();
}
}
}
/*
* Started when the observer receives a Purchase Updates Response Once the AsyncTask returns successfully, we'll
* update the UI.
*/
private class PurchaseUpdatesAsyncTask extends AsyncTask<PurchaseUpdatesResponse, Void, Boolean> {
@Override
protected Boolean doInBackground(final PurchaseUpdatesResponse... params) {
final PurchaseUpdatesResponse purchaseUpdatesResponse = params[0];
final SharedPreferences.Editor editor = getSharedPreferencesEditor();
final String userId = baseActivity.getCurrentUser();
if (!purchaseUpdatesResponse.getUserId().equals(userId)) {
return false;
}
/*
* If the customer for some reason had items revoked, the skus for these items will be contained in the
* revoked skus set.
*/
for (final String sku : purchaseUpdatesResponse.getRevokedSkus()) {
Log.v(TAG, "Revoked Sku:" + sku);
final String key = getKey(sku);
editor.putBoolean(key, false);
editor.commit();
}
switch (purchaseUpdatesResponse.getPurchaseUpdatesRequestStatus()) {
case SUCCESSFUL:
SubscriptionPeriod latestSubscriptionPeriod = null;
final LinkedList<SubscriptionPeriod> currentSubscriptionPeriods = new LinkedList<SubscriptionPeriod>();
for (final Receipt receipt : purchaseUpdatesResponse.getReceipts()) {
final String sku = receipt.getSku();
final String key = getKey(sku);
switch (receipt.getItemType()) {
case ENTITLED:
/*
* If the receipt is for an entitlement, the customer is re-entitled.
*/
editor.putBoolean(key, true);
editor.commit();
break;
case SUBSCRIPTION:
/*
* Purchase Updates for subscriptions can be done in one of two ways:
* 1. Use the receipts to determine if the user currently has an active subscription
* 2. Use the receipts to create a subscription history for your customer.
* This application checks if there is an open subscription the application uses the receipts
* returned to determine an active subscription.
* Applications that unlock content based on past active subscription periods, should create
* purchasing history for the customer.
* For example, if the customer has a magazine subscription for a year,
* even if they do not have a currently active subscription,
* they still have access to the magazines from when they were subscribed.
*/
final SubscriptionPeriod subscriptionPeriod = receipt.getSubscriptionPeriod();
final Date startDate = subscriptionPeriod.getStartDate();
/*
* Keep track of the receipt that has the most current start date.
* Store a container of duplicate subscription periods.
* If there is a duplicate, the duplicate is added to the list of current subscription periods.
*/
if (latestSubscriptionPeriod == null ||
startDate.after(latestSubscriptionPeriod.getStartDate())) {
currentSubscriptionPeriods.clear();
latestSubscriptionPeriod = subscriptionPeriod;
currentSubscriptionPeriods.add(latestSubscriptionPeriod);
} else if (startDate.equals(latestSubscriptionPeriod.getStartDate())) {
currentSubscriptionPeriods.add(receipt.getSubscriptionPeriod());
}
break;
}
printReceipt(receipt);
}
/*
* Check the latest subscription periods once all receipts have been read, if there is a subscription
* with an existing end date, then the subscription is not active.
*/
if (latestSubscriptionPeriod != null) {
boolean hasSubscription = true;
for (SubscriptionPeriod subscriptionPeriod : currentSubscriptionPeriods) {
if (subscriptionPeriod.getEndDate() != null) {
hasSubscription = false;
break;
}
}
editor.putBoolean(ButtonClickerActivity.HAS_SUBSCRIPTION, hasSubscription);
editor.commit();
}
/*
* Store the offset into shared preferences. If there has been more purchases since the
* last time our application updated, another initiatePurchaseUpdatesRequest is called with the new
* offset.
*/
final Offset newOffset = purchaseUpdatesResponse.getOffset();
editor.putString(OFFSET, newOffset.toString());
editor.commit();
if (purchaseUpdatesResponse.isMore()) {
Log.v(TAG, "Initiating Another Purchase Updates with offset: " + newOffset.toString());
PurchasingManager.initiatePurchaseUpdatesRequest(newOffset);
}
return true;
case FAILED:
/*
* On failed responses the application will ignore the request.
*/
return false;
}
return false;
}
@Override
protected void onPostExecute(final Boolean success) {
super.onPostExecute(success);
if (success) {
baseActivity.update();
}
}
}
}