/* * Copyright 2014 serso aka se.solovyev * * 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. * * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Contact details * * Email: se.solovyev@gmail.com * Site: http://se.solovyev.org */ package org.solovyev.android.checkout; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.util.SparseArray; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Variant of {@link Checkout} that can initiate a purchase. {@link ActivityCheckout} lives in the * context of {@link Activity} as it is required by Billing API to conduct a purchase. * Usage example: * <pre> * {@code * * protected final ActivityCheckout mCheckout = Checkout.forActivity(this, getBilling()); * * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * mCheckout.start(); * } * * protected void purchase(final String product, final String sku) { * mCheckout.whenReady(new Checkout.EmptyListener() { * public void onReady(BillingRequests requests) { * requests.purchase(product, sku, null, checkout.getPurchaseFlow()); * } * }); * } * * protected void onActivityResult(int requestCode, int resultCode, Intent data) { * super.onActivityResult(requestCode, resultCode, data); * mCheckout.onActivityResult(requestCode, resultCode, data); * } * * protected void onDestroy() { * mCheckout.stop(); * super.onDestroy(); * } * } * </pre> * Another usage with "one-shot" purchase flows can be found in documentation for {@link * ActivityCheckout#createOneShotPurchaseFlow(int, RequestListener)}. */ public final class ActivityCheckout extends Checkout { static final int DEFAULT_REQUEST_CODE = 0XCAFE;// mm, coffee @Nonnull private final SparseArray<PurchaseFlow> mFlows = new SparseArray<>(); ActivityCheckout(@Nullable Context context, @Nonnull Billing billing) { super(context, billing); } @Override public void stop() { mFlows.clear(); super.stop(); } /** * Same as {@link #createPurchaseFlow(int, RequestListener)} but with the default request code */ public void createPurchaseFlow(@Nonnull RequestListener<Purchase> listener) { createPurchaseFlow(DEFAULT_REQUEST_CODE, listener); } /** * Creates a permanent purchase flow with a <var>listener</var> that receives purchase * updates. Listener will receive updates only from the purchase marked with given * <var>requestCode</var>. * All flows are automatically destroyed in {@link #stop()} method. * Permanent purchase flows are not destroyed when they are finished (comparing to "one-shot" * flows), thus, <var>listener</var> methods might be called several times if several purchases * with the same <var>requestCode</var> were initiated. * * @param requestCode request code associated with a purchase * @param listener purchase listener */ public void createPurchaseFlow(int requestCode, @Nonnull RequestListener<Purchase> listener) { createPurchaseFlow(requestCode, listener, false); } /** * Same as {@link #destroyPurchaseFlow(int)} but with the default request code */ public void destroyPurchaseFlow() { destroyPurchaseFlow(DEFAULT_REQUEST_CODE); } /** * Destroys previously created purchase flow. Nothing happens if flow has already been * destroyed. * * @param requestCode purchase request code */ public void destroyPurchaseFlow(int requestCode) { final PurchaseFlow flow = mFlows.get(requestCode); if (flow == null) { return; } mFlows.delete(requestCode); // instead of cancelling purchase request in `Billing` class (which we can't do as we don't // have `requestId`) let's cancel it here flow.cancel(); } /** * Same as {@link #getPurchaseFlow(int)} with the default request code. */ @Nonnull public PurchaseFlow getPurchaseFlow() { return getPurchaseFlow(DEFAULT_REQUEST_CODE); } /** * @param requestCode request request code associated with a purchase * @return previously created purchase flow associated with <var>requestCode</var> * @throws IllegalArgumentException if purchase flow for <var>requestCode</var> doesn't exist */ @Nonnull public PurchaseFlow getPurchaseFlow(int requestCode) { final PurchaseFlow flow = mFlows.get(requestCode); if (flow == null) { throw new IllegalArgumentException("Purchase flow doesn't exist. Have you forgotten to create it?"); } return flow; } /** * Same as {@link #createOneShotPurchaseFlow(int, RequestListener)} with the default request * code. */ @Nonnull public PurchaseFlow createOneShotPurchaseFlow(@Nonnull RequestListener<Purchase> listener) { return createOneShotPurchaseFlow(DEFAULT_REQUEST_CODE, listener); } /** * Creates a new "one-shot" purchase flow associated with the given <var>requestCode</var> and * <var>listener</var>. As soon as the flow is finished it is destroyed and <var>listener</var> * is unregistered. Next purchase should be initiated with a new purchase flow. This might * be useful if activity is never destroyed - then instead of calling {@link * ActivityCheckout#createPurchaseFlow(int, RequestListener)} in {@link * Activity#onCreate(android.os.Bundle)} and {@link ActivityCheckout#getPurchaseFlow()} * while starting a purchase flow only this method might be used: * <pre> * {@code * * protected final ActivityCheckout mCheckout = Checkout.forActivity(this, getBilling()); * * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * // ... * mCheckout.start(); * // NOTE: we don't need to create purchase flow here, it is created in `purchase` method * } * * protected void purchase(final String product, final String sku) { * mCheckout.whenReady(new Checkout.EmptyListener() { * public void onReady(BillingRequests requests) { * // listener will be unregistered when the purchase flow finishes. If this * // method is called several times with the same requestCode an exception is * // raised * requests.purchase(product, sku, null, checkout.createOneShotPurchaseFlow(createPurchaseListener())); * } * }); * } * } * </pre> * <p/> * See {@link ActivityCheckout#createPurchaseFlow(int, RequestListener)} for creating a * permanent purchase flows. * * @param requestCode request code associated with a purchase * @param listener purchase listener * @return newly created "one-shot" purchase flow * @throws IllegalArgumentException if purchase flow for <var>requestCode</var> already exists */ @Nonnull public PurchaseFlow createOneShotPurchaseFlow(int requestCode, @Nonnull RequestListener<Purchase> listener) { return createPurchaseFlow(requestCode, listener, true); } @Nonnull private PurchaseFlow createPurchaseFlow(final int requestCode, @Nonnull RequestListener<Purchase> listener, boolean oneShot) { PurchaseFlow flow = mFlows.get(requestCode); if (flow != null) { throw new IllegalArgumentException("Purchase flow associated with requestCode=" + requestCode + " already exists"); } if (oneShot) { listener = new OneShotRequestListener(listener, requestCode); } flow = mBilling.createPurchaseFlow(getActivity(), requestCode, listener); mFlows.append(requestCode, flow); return flow; } /** * Creates a one-shot {@link PurchaseFlow} and tries starting it. If {@link Checkout} is not * ready the start is postponed. */ public void startPurchaseFlow(final String product, final String sku, @Nullable final String payload, final RequestListener<Purchase> listener) { createOneShotPurchaseFlow(listener); whenReady(new EmptyListener() { @Override public void onReady(@Nonnull BillingRequests requests) { requests.purchase(product, sku, payload, getPurchaseFlow()); } }); } /** * @see #startPurchaseFlow(String, String, String, RequestListener) */ public void startPurchaseFlow(final Sku sku, @Nullable final String payload, final RequestListener<Purchase> listener) { startPurchaseFlow(sku.id.product, sku.id.code, payload, listener); } private Activity getActivity() { return (Activity) mContext; } /** * This method must be called from {@link Activity#onActivityResult(int, int, Intent)} in order * to finish a purchase flow. * * @return true if activity result was handled (there existed a purchase flow for the given * <var>requestCode</var>) * @see Activity#onActivityResult(int, int, Intent) */ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { final PurchaseFlow flow = mFlows.get(requestCode); if (flow == null) { Billing.warning("Purchase flow doesn't exist for requestCode=" + requestCode + ". Have you forgotten to create it?"); return false; } flow.onActivityResult(requestCode, resultCode, data); return true; } private class OneShotRequestListener extends RequestListenerWrapper<Purchase> { private final int mRequestCode; public OneShotRequestListener(RequestListener<Purchase> listener, int requestCode) { super(listener); mRequestCode = requestCode; } @Override public void onError(int response, @Nonnull Exception e) { destroyPurchaseFlow(mRequestCode); super.onError(response, e); } @Override public void onCancel() { destroyPurchaseFlow(mRequestCode); } @Override public void onSuccess(@Nonnull Purchase result) { destroyPurchaseFlow(mRequestCode); super.onSuccess(result); } } }