/* * 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 org.json.JSONException; import android.app.Activity; import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; import static android.app.Activity.RESULT_OK; import static java.util.Collections.singletonList; import static org.solovyev.android.checkout.ResponseCodes.EXCEPTION; import static org.solovyev.android.checkout.ResponseCodes.NULL_INTENT; import static org.solovyev.android.checkout.ResponseCodes.OK; import static org.solovyev.android.checkout.ResponseCodes.WRONG_SIGNATURE; /** * <p> * Class that represents one purchase flow (process) from the moment when user requests a purchase * until the moment the purchase goes through. It is mainly used by {@link * BillingRequests#purchase(Sku, String, PurchaseFlow)} * in order to conduct a purchase. This class can only be instantiated in the context of * {@link Activity} as it is required by Billing API (to start a Google Play app). * </p> * There are three main steps in the purchase process: * <ol> * <li>Initial communication with the billing service and preparing the purchase (done in {@link * BillingRequests#purchase(Sku, String, PurchaseFlow)})</li> * <li>Starting Google Play app to conduct the purchase (done in this class, see {@link * PurchaseFlow#onSuccess(PendingIntent)}</li> * <li>Handling the result from Google Play app (done in this class, see {@link * PurchaseFlow#onActivityResult(int, int, Intent)})</li> * </ol> */ public final class PurchaseFlow implements CancellableRequestListener<PendingIntent> { static final String EXTRA_RESPONSE = "RESPONSE_CODE"; static final String EXTRA_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; static final String EXTRA_PURCHASE_SIGNATURE = "INAPP_DATA_SIGNATURE"; @Nonnull private final Activity mActivity; private final int mRequestCode; @Nonnull private final PurchaseVerifier mVerifier; @Nullable private RequestListener<Purchase> mListener; PurchaseFlow(@Nonnull Activity activity, int requestCode, @Nonnull RequestListener<Purchase> listener, @Nonnull PurchaseVerifier verifier) { mActivity = activity; mRequestCode = requestCode; mListener = listener; mVerifier = verifier; } @Override public void onSuccess(@Nonnull PendingIntent purchaseIntent) { if (mListener == null) { // request was cancelled => stop here return; } try { mActivity.startIntentSenderForResult(purchaseIntent.getIntentSender(), mRequestCode, new Intent(), 0, 0, 0); } catch (RuntimeException | IntentSender.SendIntentException e) { handleError(e); } } void onActivityResult(int requestCode, int resultCode, Intent intent) { try { Check.equals(mRequestCode, requestCode); if (intent == null) { // sometimes intent is null (it's not obvious when it happens but it happens from time to time) handleError(NULL_INTENT); return; } final int responseCode = intent.getIntExtra(EXTRA_RESPONSE, OK); if (resultCode != RESULT_OK || responseCode != OK) { handleError(responseCode); return; } final String data = intent.getStringExtra(EXTRA_PURCHASE_DATA); final String signature = intent.getStringExtra(EXTRA_PURCHASE_SIGNATURE); Check.isNotNull(data); Check.isNotNull(signature); final Purchase purchase = Purchase.fromJson(data, signature); mVerifier.verify(singletonList(purchase), new VerificationListener()); } catch (RuntimeException | JSONException e) { handleError(e); } } private void handleError(int response) { Billing.error("Error response: " + response + " in Purchase/ChangePurchase request"); onError(response, new BillingException(response)); } private void handleError(@Nonnull Exception e) { Billing.error("Exception in Purchase/ChangePurchase request: ", e); onError(ResponseCodes.EXCEPTION, e); } @Override public void onError(int response, @Nonnull Exception e) { if (mListener == null) { return; } mListener.onError(response, e); } /** * Cancels this purchase flow. * Note that cancelling the purchase flow is not the same as cancelling the purchase process as * purchase process is not controlled by the app. This method only guarantees that there will be * no more calls of {@link RequestListener}'s methods. */ @Override public void cancel() { if (mListener == null) { return; } Billing.cancel(mListener); mListener = null; } private class VerificationListener implements RequestListener<List<Purchase>> { @Override public void onSuccess(@Nonnull List<Purchase> verifiedPurchases) { Check.isMainThread(); if (verifiedPurchases.isEmpty()) { handleError(WRONG_SIGNATURE); return; } if (mListener == null) { return; } mListener.onSuccess(verifiedPurchases.get(0)); } @Override public void onError(int response, @Nonnull Exception e) { Check.isMainThread(); if (response == EXCEPTION) { handleError(e); } else { handleError(response); } } } }