Java tutorial
/* * 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.os.Bundle; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** * List of purchased items of <var>product</var> type. */ @Immutable public final class Purchases { static final String BUNDLE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; static final String BUNDLE_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; static final String BUNDLE_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; /** * Product type */ @Nonnull public final String product; /** * Purchased items */ @Nonnull public final List<Purchase> list; /** * Token to be used to request more purchases, see <a href="http://developer.android.com/google/play/billing/billing_integrate.html#QueryPurchases">Query Purchases</a> docs. * * @see BillingRequests#getPurchases(String, String, RequestListener) */ @Nullable public final String continuationToken; Purchases(@Nonnull String product, @Nonnull List<Purchase> list, @Nullable String continuationToken) { this.product = product; this.list = Collections.unmodifiableList(list); this.continuationToken = continuationToken; } @Nonnull static Purchases fromBundle(@Nonnull Bundle bundle, @Nonnull String product) throws JSONException { final String continuationToken = getContinuationTokenFromBundle(bundle); final List<Purchase> purchases = getListFromBundle(bundle); return new Purchases(product, purchases, continuationToken); } @Nullable static String getContinuationTokenFromBundle(@Nonnull Bundle bundle) { return bundle.getString(BUNDLE_CONTINUATION_TOKEN); } @Nonnull static List<Purchase> getListFromBundle(@Nonnull Bundle bundle) throws JSONException { final List<String> datas = extractDatasList(bundle); final List<String> signatures = bundle.getStringArrayList(BUNDLE_SIGNATURE_LIST); final List<Purchase> purchases = new ArrayList<Purchase>(datas.size()); for (int i = 0; i < datas.size(); i++) { final String data = datas.get(i); final String signature = signatures != null ? signatures.get(i) : ""; purchases.add(Purchase.fromJson(data, signature)); } return purchases; } /** * Same as {@link #toJson(boolean)} with {@code withSignatures=false} * @return JSON representation of this object */ @Nonnull public String toJson() { return toJson(false); } /** * @param withSignatures if true then {@link Purchase} will include signature field * @return JSON representation of this object */ @Nonnull public String toJson(boolean withSignatures) { return toJsonObject(withSignatures).toString(); } @Nonnull JSONObject toJsonObject(boolean withSignatures) { final JSONObject json = new JSONObject(); try { json.put("product", product); final JSONArray array = new JSONArray(); for (int i = 0; i < list.size(); i++) { final Purchase purchase = list.get(i); array.put(i, purchase.toJsonObject(withSignatures)); } json.put("list", array); } catch (JSONException e) { // should never happen throw new AssertionError(e); } return json; } @Nonnull private static List<String> extractDatasList(@Nonnull Bundle bundle) { final List<String> list = bundle.getStringArrayList(BUNDLE_DATA_LIST); return list != null ? list : Collections.<String>emptyList(); } @Nullable public Purchase getPurchase(@Nonnull String sku) { for (Purchase purchase : list) { if (purchase.sku.equals(sku)) { return purchase; } } return null; } /** * <b>Note</b>: this method doesn't check state of the purchase * * @param sku SKU of purchase to be found * @return true if purchase with specified <var>sku</var> exists */ public boolean hasPurchase(@Nonnull String sku) { return getPurchase(sku) != null; } /** * @param sku SKU of purchase to be found * @param state state of the purchase to be found * @return true if purchase with specified <var>sku</var> and <var>state</var> exists */ public boolean hasPurchaseInState(@Nonnull String sku, @Nonnull Purchase.State state) { return getPurchaseInState(sku, state) != null; } @Nullable public Purchase getPurchaseInState(@Nonnull String sku, @Nonnull Purchase.State state) { return getPurchaseInState(list, sku, state); } @Nullable static Purchase getPurchaseInState(@Nonnull List<Purchase> purchases, @Nonnull String sku, @Nonnull Purchase.State state) { for (Purchase purchase : purchases) { if (purchase.sku.equals(sku)) { if (purchase.state == state) { return purchase; } } } return null; } @Nonnull static List<Purchase> neutralize(@Nonnull List<Purchase> purchases) { // probably, it's possible to avoid creation of temporary list. The reason for it is that we don't want to // modify original list purchases = new LinkedList<Purchase>(purchases); final List<Purchase> result = new ArrayList<Purchase>(purchases.size()); Collections.sort(purchases, PurchaseComparator.earliestFirst()); while (!purchases.isEmpty()) { final Purchase purchase = purchases.get(0); switch (purchase.state) { case PURCHASED: if (!isNeutralized(purchases, purchase)) { result.add(purchase); } break; case CANCELLED: case REFUNDED: case EXPIRED: if (!isDangling(purchases, purchase)) { result.add(purchase); } break; } purchases.remove(0); } // purchases were added earliest first but we want result to be latest first Collections.reverse(result); return result; } private static boolean isDangling(@Nonnull List<Purchase> purchases, @Nonnull Purchase purchase) { Check.isFalse(purchase.state == Purchase.State.PURCHASED, "Must not be PURCHASED"); for (int i = 1; i < purchases.size(); i++) { final Purchase same = purchases.get(i); if (same.sku.equals(purchase.sku)) { // for not purchases transaction exists newer transaction => this transaction is dangling return true; } } return false; } private static boolean isNeutralized(@Nonnull List<Purchase> purchases, @Nonnull Purchase purchase) { Check.isTrue(purchase.state == Purchase.State.PURCHASED, "Must be PURCHASED"); for (int i = 1; i < purchases.size(); i++) { final Purchase same = purchases.get(i); if (same.sku.equals(purchase.sku)) { switch (same.state) { case PURCHASED: // found same later purchase => obviously there is a bug somewhere as user can't own // several purchases with same SKU. For now let's skip the item Billing.warning("Two purchases with same SKU found: " + purchase + " and " + same); break; case CANCELLED: case REFUNDED: case EXPIRED: // neutralization found => need to remove it purchases.remove(i); break; } return true; } } return false; } }